tlc-claude-code 1.6.4 → 1.7.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/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/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/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/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/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,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Command
|
|
3
|
+
*
|
|
4
|
+
* /tlc:gate command to install, configure, and run the code gate.
|
|
5
|
+
* Subcommands: install, check, status, config
|
|
6
|
+
*
|
|
7
|
+
* @module code-gate/gate-command
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse gate command arguments into structured options.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} args - Raw argument string
|
|
14
|
+
* @returns {{ subcommand: string }}
|
|
15
|
+
*/
|
|
16
|
+
function parseGateArgs(args) {
|
|
17
|
+
const trimmed = (args || '').trim();
|
|
18
|
+
const subcommand = trimmed.split(/\s+/)[0] || 'check';
|
|
19
|
+
|
|
20
|
+
const valid = ['install', 'check', 'status', 'config'];
|
|
21
|
+
return {
|
|
22
|
+
subcommand: valid.includes(subcommand) ? subcommand : 'check',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a gate command with injectable dependencies.
|
|
28
|
+
* This allows testing without real file system or git operations.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object} deps - Dependencies
|
|
31
|
+
* @param {string} deps.projectPath - Project root path
|
|
32
|
+
* @param {Function} [deps.installHooks] - Hook installer function
|
|
33
|
+
* @param {Function} [deps.runGate] - Gate engine runner function
|
|
34
|
+
* @param {Function} [deps.getStagedFiles] - Get staged files function
|
|
35
|
+
* @param {Function} [deps.loadConfig] - Config loader function
|
|
36
|
+
* @param {Function} [deps.saveConfig] - Config saver function
|
|
37
|
+
* @param {Function} [deps.isHookInstalled] - Hook check function
|
|
38
|
+
* @returns {{ execute: Function }}
|
|
39
|
+
*/
|
|
40
|
+
function createGateCommand(deps) {
|
|
41
|
+
const {
|
|
42
|
+
projectPath,
|
|
43
|
+
installHooks,
|
|
44
|
+
runGate,
|
|
45
|
+
getStagedFiles,
|
|
46
|
+
loadConfig,
|
|
47
|
+
saveConfig,
|
|
48
|
+
isHookInstalled,
|
|
49
|
+
} = deps;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
/**
|
|
53
|
+
* Execute a gate subcommand.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} subcommand - install|check|status|config
|
|
56
|
+
* @param {Object} [options] - Subcommand-specific options
|
|
57
|
+
* @returns {Promise<Object>} Subcommand result
|
|
58
|
+
*/
|
|
59
|
+
async execute(subcommand, options = {}) {
|
|
60
|
+
switch (subcommand) {
|
|
61
|
+
case 'install':
|
|
62
|
+
return handleInstall();
|
|
63
|
+
case 'check':
|
|
64
|
+
return handleCheck();
|
|
65
|
+
case 'status':
|
|
66
|
+
return handleStatus();
|
|
67
|
+
case 'config':
|
|
68
|
+
return handleConfig(options);
|
|
69
|
+
default:
|
|
70
|
+
return { success: false, error: `Unknown subcommand: ${subcommand}` };
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
async function handleInstall() {
|
|
76
|
+
if (!installHooks) {
|
|
77
|
+
return { success: false, error: 'Hook installer not available' };
|
|
78
|
+
}
|
|
79
|
+
const result = await installHooks(projectPath);
|
|
80
|
+
return { success: true, installed: result.installed };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleCheck() {
|
|
84
|
+
if (!runGate) {
|
|
85
|
+
return { success: false, error: 'Gate engine not available' };
|
|
86
|
+
}
|
|
87
|
+
const files = getStagedFiles ? await getStagedFiles() : [];
|
|
88
|
+
return await runGate(files);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function handleStatus() {
|
|
92
|
+
const config = loadConfig ? loadConfig(projectPath) : {};
|
|
93
|
+
const hooks = {
|
|
94
|
+
'pre-commit': isHookInstalled ? isHookInstalled(projectPath, 'pre-commit') : false,
|
|
95
|
+
'pre-push': isHookInstalled ? isHookInstalled(projectPath, 'pre-push') : false,
|
|
96
|
+
};
|
|
97
|
+
return { config, hooks };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function handleConfig(updates) {
|
|
101
|
+
if (!saveConfig) {
|
|
102
|
+
return { success: false, error: 'Config saver not available' };
|
|
103
|
+
}
|
|
104
|
+
const current = loadConfig ? loadConfig(projectPath) : {};
|
|
105
|
+
const merged = { ...current, ...updates };
|
|
106
|
+
saveConfig(merged);
|
|
107
|
+
return { success: true, config: merged };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
createGateCommand,
|
|
113
|
+
parseGateArgs,
|
|
114
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Command Tests
|
|
3
|
+
*
|
|
4
|
+
* /tlc:gate command to install, configure, and run the code gate.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
createGateCommand,
|
|
10
|
+
parseGateArgs,
|
|
11
|
+
} = require('./gate-command.js');
|
|
12
|
+
|
|
13
|
+
describe('Gate Command', () => {
|
|
14
|
+
describe('parseGateArgs', () => {
|
|
15
|
+
it('parses install subcommand', () => {
|
|
16
|
+
const args = parseGateArgs('install');
|
|
17
|
+
expect(args.subcommand).toBe('install');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('parses check subcommand', () => {
|
|
21
|
+
const args = parseGateArgs('check');
|
|
22
|
+
expect(args.subcommand).toBe('check');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('parses status subcommand', () => {
|
|
26
|
+
const args = parseGateArgs('status');
|
|
27
|
+
expect(args.subcommand).toBe('status');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('defaults to check when no subcommand', () => {
|
|
31
|
+
const args = parseGateArgs('');
|
|
32
|
+
expect(args.subcommand).toBe('check');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('parses config subcommand', () => {
|
|
36
|
+
const args = parseGateArgs('config');
|
|
37
|
+
expect(args.subcommand).toBe('config');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('createGateCommand', () => {
|
|
42
|
+
it('creates command with injectable dependencies', () => {
|
|
43
|
+
const cmd = createGateCommand({ projectPath: '/test' });
|
|
44
|
+
expect(cmd).toBeDefined();
|
|
45
|
+
expect(cmd.execute).toBeTypeOf('function');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('install subcommand calls hooks installer', async () => {
|
|
49
|
+
const mockInstallHooks = vi.fn().mockResolvedValue({ installed: ['pre-commit', 'pre-push'] });
|
|
50
|
+
const cmd = createGateCommand({
|
|
51
|
+
projectPath: '/test',
|
|
52
|
+
installHooks: mockInstallHooks,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const result = await cmd.execute('install');
|
|
56
|
+
expect(mockInstallHooks).toHaveBeenCalled();
|
|
57
|
+
expect(result.success).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('check subcommand runs gate engine', async () => {
|
|
61
|
+
const mockRunGate = vi.fn().mockResolvedValue({
|
|
62
|
+
passed: true,
|
|
63
|
+
findings: [],
|
|
64
|
+
summary: { total: 0, block: 0, warn: 0, info: 0 },
|
|
65
|
+
});
|
|
66
|
+
const mockGetStagedFiles = vi.fn().mockResolvedValue([]);
|
|
67
|
+
const cmd = createGateCommand({
|
|
68
|
+
projectPath: '/test',
|
|
69
|
+
runGate: mockRunGate,
|
|
70
|
+
getStagedFiles: mockGetStagedFiles,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const result = await cmd.execute('check');
|
|
74
|
+
expect(result.passed).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('status subcommand returns gate configuration', async () => {
|
|
78
|
+
const mockLoadConfig = vi.fn().mockReturnValue({
|
|
79
|
+
enabled: true,
|
|
80
|
+
strictness: 'strict',
|
|
81
|
+
preCommit: true,
|
|
82
|
+
prePush: true,
|
|
83
|
+
});
|
|
84
|
+
const mockIsInstalled = vi.fn().mockReturnValue(true);
|
|
85
|
+
const cmd = createGateCommand({
|
|
86
|
+
projectPath: '/test',
|
|
87
|
+
loadConfig: mockLoadConfig,
|
|
88
|
+
isHookInstalled: mockIsInstalled,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const result = await cmd.execute('status');
|
|
92
|
+
expect(result.config).toBeDefined();
|
|
93
|
+
expect(result.config.strictness).toBe('strict');
|
|
94
|
+
expect(result.hooks).toBeDefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('config subcommand updates .tlc.json', async () => {
|
|
98
|
+
let savedConfig = null;
|
|
99
|
+
const mockSaveConfig = vi.fn((config) => { savedConfig = config; });
|
|
100
|
+
const cmd = createGateCommand({
|
|
101
|
+
projectPath: '/test',
|
|
102
|
+
saveConfig: mockSaveConfig,
|
|
103
|
+
loadConfig: vi.fn().mockReturnValue({ enabled: true, strictness: 'strict' }),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await cmd.execute('config', { strictness: 'standard' });
|
|
107
|
+
expect(mockSaveConfig).toHaveBeenCalled();
|
|
108
|
+
expect(result.success).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Configuration
|
|
3
|
+
*
|
|
4
|
+
* Reads gate config from .tlc.json, supports rule enable/disable,
|
|
5
|
+
* severity overrides, ignore patterns, and strictness levels.
|
|
6
|
+
*
|
|
7
|
+
* @module code-gate/gate-config
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
/** Strictness levels control default severity thresholds */
|
|
14
|
+
const STRICTNESS = {
|
|
15
|
+
RELAXED: 'relaxed',
|
|
16
|
+
STANDARD: 'standard',
|
|
17
|
+
STRICT: 'strict',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Return the default gate configuration.
|
|
22
|
+
* Defaults to strict mode — block on all high/critical findings.
|
|
23
|
+
*
|
|
24
|
+
* @returns {Object} Default gate config
|
|
25
|
+
*/
|
|
26
|
+
function getDefaultGateConfig() {
|
|
27
|
+
return {
|
|
28
|
+
enabled: true,
|
|
29
|
+
strictness: STRICTNESS.STRICT,
|
|
30
|
+
preCommit: true,
|
|
31
|
+
prePush: true,
|
|
32
|
+
rules: {},
|
|
33
|
+
ignore: ['*.md', '*.json', '*.lock', '*.yml', '*.yaml'],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load gate configuration from .tlc.json in the given project path.
|
|
39
|
+
* Falls back to defaults if no config exists or if parsing fails.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} projectPath - Path to project root
|
|
42
|
+
* @param {Object} [options] - Options with injectable dependencies
|
|
43
|
+
* @param {Object} [options.fs] - File system module (for testing)
|
|
44
|
+
* @returns {Object} Resolved gate configuration
|
|
45
|
+
*/
|
|
46
|
+
function loadGateConfig(projectPath, options = {}) {
|
|
47
|
+
const fsModule = options.fs || fs;
|
|
48
|
+
const defaults = getDefaultGateConfig();
|
|
49
|
+
const configPath = path.join(projectPath, '.tlc.json');
|
|
50
|
+
|
|
51
|
+
if (!fsModule.existsSync(configPath)) {
|
|
52
|
+
return defaults;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const raw = fsModule.readFileSync(configPath, 'utf-8');
|
|
57
|
+
const parsed = JSON.parse(raw);
|
|
58
|
+
const gateSection = parsed.gate || {};
|
|
59
|
+
return mergeGateConfig(defaults, gateSection);
|
|
60
|
+
} catch {
|
|
61
|
+
return defaults;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Deep-merge user gate config over defaults.
|
|
67
|
+
* Rules are merged as an override map. Ignore arrays are concatenated
|
|
68
|
+
* with deduplication.
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} defaults - Default gate config
|
|
71
|
+
* @param {Object} userConfig - User overrides from .tlc.json
|
|
72
|
+
* @returns {Object} Merged config
|
|
73
|
+
*/
|
|
74
|
+
function mergeGateConfig(defaults, userConfig) {
|
|
75
|
+
const merged = { ...defaults };
|
|
76
|
+
|
|
77
|
+
// Simple scalar overrides
|
|
78
|
+
if (userConfig.enabled !== undefined) merged.enabled = userConfig.enabled;
|
|
79
|
+
if (userConfig.strictness !== undefined) merged.strictness = userConfig.strictness;
|
|
80
|
+
if (userConfig.preCommit !== undefined) merged.preCommit = userConfig.preCommit;
|
|
81
|
+
if (userConfig.prePush !== undefined) merged.prePush = userConfig.prePush;
|
|
82
|
+
|
|
83
|
+
// Merge rules as override map
|
|
84
|
+
if (userConfig.rules) {
|
|
85
|
+
merged.rules = { ...defaults.rules, ...userConfig.rules };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Concatenate ignore arrays, deduplicate
|
|
89
|
+
if (userConfig.ignore) {
|
|
90
|
+
const combined = [...defaults.ignore, ...userConfig.ignore];
|
|
91
|
+
merged.ignore = [...new Set(combined)];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return merged;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the effective severity for a rule, considering user overrides.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} ruleId - Rule identifier
|
|
101
|
+
* @param {string} defaultSeverity - The rule's built-in severity
|
|
102
|
+
* @param {Object} config - Gate config with rules overrides
|
|
103
|
+
* @returns {string|false} Effective severity, or false if rule is disabled
|
|
104
|
+
*/
|
|
105
|
+
function resolveRuleSeverity(ruleId, defaultSeverity, config) {
|
|
106
|
+
if (config.rules && config.rules[ruleId] !== undefined) {
|
|
107
|
+
return config.rules[ruleId];
|
|
108
|
+
}
|
|
109
|
+
return defaultSeverity;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if a file should be ignored based on config ignore patterns.
|
|
114
|
+
* Supports simple glob matching: *.ext and dir/* patterns.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} filePath - File path to check
|
|
117
|
+
* @param {Object} config - Gate config with ignore array
|
|
118
|
+
* @returns {boolean} True if file should be ignored
|
|
119
|
+
*/
|
|
120
|
+
function shouldIgnoreFile(filePath, config) {
|
|
121
|
+
const patterns = config.ignore || [];
|
|
122
|
+
for (const pattern of patterns) {
|
|
123
|
+
if (matchPattern(filePath, pattern)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Simple glob pattern matcher.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} filePath
|
|
134
|
+
* @param {string} pattern
|
|
135
|
+
* @returns {boolean}
|
|
136
|
+
*/
|
|
137
|
+
function matchPattern(filePath, pattern) {
|
|
138
|
+
// *.ext — match extension anywhere
|
|
139
|
+
if (pattern.startsWith('*.')) {
|
|
140
|
+
return filePath.endsWith(pattern.slice(1));
|
|
141
|
+
}
|
|
142
|
+
// **/*.ext — match extension in any nested path
|
|
143
|
+
if (pattern.startsWith('**/')) {
|
|
144
|
+
const sub = pattern.slice(3);
|
|
145
|
+
return matchPattern(filePath, sub) || matchPattern(path.basename(filePath), sub);
|
|
146
|
+
}
|
|
147
|
+
// dir/* — match files starting with dir/
|
|
148
|
+
if (pattern.endsWith('/*')) {
|
|
149
|
+
const dir = pattern.slice(0, -2);
|
|
150
|
+
return filePath.startsWith(dir + '/');
|
|
151
|
+
}
|
|
152
|
+
// Exact match
|
|
153
|
+
return filePath === pattern;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
loadGateConfig,
|
|
158
|
+
getDefaultGateConfig,
|
|
159
|
+
mergeGateConfig,
|
|
160
|
+
resolveRuleSeverity,
|
|
161
|
+
shouldIgnoreFile,
|
|
162
|
+
STRICTNESS,
|
|
163
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Configuration Tests
|
|
3
|
+
*
|
|
4
|
+
* Reads gate config from .tlc.json, supports rule enable/disable,
|
|
5
|
+
* severity overrides, ignore patterns, and strictness levels.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
loadGateConfig,
|
|
11
|
+
getDefaultGateConfig,
|
|
12
|
+
mergeGateConfig,
|
|
13
|
+
resolveRuleSeverity,
|
|
14
|
+
shouldIgnoreFile,
|
|
15
|
+
STRICTNESS,
|
|
16
|
+
} = require('./gate-config.js');
|
|
17
|
+
|
|
18
|
+
describe('Gate Configuration', () => {
|
|
19
|
+
describe('STRICTNESS', () => {
|
|
20
|
+
it('defines all strictness levels', () => {
|
|
21
|
+
expect(STRICTNESS.RELAXED).toBe('relaxed');
|
|
22
|
+
expect(STRICTNESS.STANDARD).toBe('standard');
|
|
23
|
+
expect(STRICTNESS.STRICT).toBe('strict');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('getDefaultGateConfig', () => {
|
|
28
|
+
it('returns default config with strict mode', () => {
|
|
29
|
+
const config = getDefaultGateConfig();
|
|
30
|
+
expect(config.enabled).toBe(true);
|
|
31
|
+
expect(config.strictness).toBe('strict');
|
|
32
|
+
expect(config.preCommit).toBe(true);
|
|
33
|
+
expect(config.prePush).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('includes default ignore patterns', () => {
|
|
37
|
+
const config = getDefaultGateConfig();
|
|
38
|
+
expect(config.ignore).toContain('*.md');
|
|
39
|
+
expect(config.ignore).toContain('*.json');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('has empty rules override by default', () => {
|
|
43
|
+
const config = getDefaultGateConfig();
|
|
44
|
+
expect(config.rules).toEqual({});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('loadGateConfig', () => {
|
|
49
|
+
it('returns defaults when no .tlc.json exists', () => {
|
|
50
|
+
const mockFs = {
|
|
51
|
+
existsSync: vi.fn().mockReturnValue(false),
|
|
52
|
+
};
|
|
53
|
+
const config = loadGateConfig('/project', { fs: mockFs });
|
|
54
|
+
expect(config.enabled).toBe(true);
|
|
55
|
+
expect(config.strictness).toBe('strict');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('reads gate section from .tlc.json', () => {
|
|
59
|
+
const tlcJson = {
|
|
60
|
+
gate: {
|
|
61
|
+
enabled: true,
|
|
62
|
+
strictness: 'standard',
|
|
63
|
+
rules: { 'no-hardcoded-urls': 'warn' },
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const mockFs = {
|
|
67
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
68
|
+
readFileSync: vi.fn().mockReturnValue(JSON.stringify(tlcJson)),
|
|
69
|
+
};
|
|
70
|
+
const config = loadGateConfig('/project', { fs: mockFs });
|
|
71
|
+
expect(config.strictness).toBe('standard');
|
|
72
|
+
expect(config.rules['no-hardcoded-urls']).toBe('warn');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('merges user config with defaults', () => {
|
|
76
|
+
const tlcJson = {
|
|
77
|
+
gate: {
|
|
78
|
+
strictness: 'relaxed',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const mockFs = {
|
|
82
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
83
|
+
readFileSync: vi.fn().mockReturnValue(JSON.stringify(tlcJson)),
|
|
84
|
+
};
|
|
85
|
+
const config = loadGateConfig('/project', { fs: mockFs });
|
|
86
|
+
// User override
|
|
87
|
+
expect(config.strictness).toBe('relaxed');
|
|
88
|
+
// Defaults preserved
|
|
89
|
+
expect(config.enabled).toBe(true);
|
|
90
|
+
expect(config.preCommit).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('handles malformed .tlc.json gracefully', () => {
|
|
94
|
+
const mockFs = {
|
|
95
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
96
|
+
readFileSync: vi.fn().mockReturnValue('not valid json{{{'),
|
|
97
|
+
};
|
|
98
|
+
const config = loadGateConfig('/project', { fs: mockFs });
|
|
99
|
+
expect(config.enabled).toBe(true);
|
|
100
|
+
expect(config.strictness).toBe('strict');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('handles missing gate section gracefully', () => {
|
|
104
|
+
const mockFs = {
|
|
105
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
106
|
+
readFileSync: vi.fn().mockReturnValue(JSON.stringify({ project: 'test' })),
|
|
107
|
+
};
|
|
108
|
+
const config = loadGateConfig('/project', { fs: mockFs });
|
|
109
|
+
expect(config.strictness).toBe('strict');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('mergeGateConfig', () => {
|
|
114
|
+
it('overrides defaults with user values', () => {
|
|
115
|
+
const defaults = getDefaultGateConfig();
|
|
116
|
+
const userConfig = { strictness: 'relaxed', prePush: false };
|
|
117
|
+
const merged = mergeGateConfig(defaults, userConfig);
|
|
118
|
+
expect(merged.strictness).toBe('relaxed');
|
|
119
|
+
expect(merged.prePush).toBe(false);
|
|
120
|
+
expect(merged.preCommit).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('merges rules as override map', () => {
|
|
124
|
+
const defaults = getDefaultGateConfig();
|
|
125
|
+
const userConfig = { rules: { 'no-eval': 'warn', 'no-hardcoded-urls': 'info' } };
|
|
126
|
+
const merged = mergeGateConfig(defaults, userConfig);
|
|
127
|
+
expect(merged.rules['no-eval']).toBe('warn');
|
|
128
|
+
expect(merged.rules['no-hardcoded-urls']).toBe('info');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('concatenates ignore arrays', () => {
|
|
132
|
+
const defaults = getDefaultGateConfig();
|
|
133
|
+
const userConfig = { ignore: ['migrations/*', 'generated/*'] };
|
|
134
|
+
const merged = mergeGateConfig(defaults, userConfig);
|
|
135
|
+
expect(merged.ignore).toContain('*.md');
|
|
136
|
+
expect(merged.ignore).toContain('migrations/*');
|
|
137
|
+
expect(merged.ignore).toContain('generated/*');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('resolveRuleSeverity', () => {
|
|
142
|
+
it('returns rule default when no override', () => {
|
|
143
|
+
const config = getDefaultGateConfig();
|
|
144
|
+
expect(resolveRuleSeverity('no-eval', 'block', config)).toBe('block');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('returns override when configured', () => {
|
|
148
|
+
const config = { ...getDefaultGateConfig(), rules: { 'no-eval': 'warn' } };
|
|
149
|
+
expect(resolveRuleSeverity('no-eval', 'block', config)).toBe('warn');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('disables rule when override is false', () => {
|
|
153
|
+
const config = { ...getDefaultGateConfig(), rules: { 'no-eval': false } };
|
|
154
|
+
expect(resolveRuleSeverity('no-eval', 'block', config)).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('shouldIgnoreFile', () => {
|
|
159
|
+
it('ignores files matching patterns', () => {
|
|
160
|
+
const config = { ignore: ['*.md', 'dist/*'] };
|
|
161
|
+
expect(shouldIgnoreFile('README.md', config)).toBe(true);
|
|
162
|
+
expect(shouldIgnoreFile('dist/bundle.js', config)).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('does not ignore non-matching files', () => {
|
|
166
|
+
const config = { ignore: ['*.md'] };
|
|
167
|
+
expect(shouldIgnoreFile('src/app.js', config)).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('handles empty ignore list', () => {
|
|
171
|
+
const config = { ignore: [] };
|
|
172
|
+
expect(shouldIgnoreFile('anything.js', config)).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('matches nested glob patterns', () => {
|
|
176
|
+
const config = { ignore: ['**/*.test.js'] };
|
|
177
|
+
expect(shouldIgnoreFile('src/deep/file.test.js', config)).toBe(true);
|
|
178
|
+
expect(shouldIgnoreFile('src/deep/file.js', config)).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|