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.
Files changed (81) hide show
  1. package/dashboard/dist/components/ContainerSecurityPane.d.ts +45 -0
  2. package/dashboard/dist/components/ContainerSecurityPane.js +44 -0
  3. package/dashboard/dist/components/ContainerSecurityPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/ContainerSecurityPane.test.js +153 -0
  5. package/package.json +1 -1
  6. package/server/lib/access-control.test.js +1 -1
  7. package/server/lib/agents-cancel-command.test.js +1 -1
  8. package/server/lib/agents-get-command.test.js +1 -1
  9. package/server/lib/agents-list-command.test.js +1 -1
  10. package/server/lib/agents-logs-command.test.js +1 -1
  11. package/server/lib/agents-retry-command.test.js +1 -1
  12. package/server/lib/budget-limits.test.js +2 -2
  13. package/server/lib/code-gate/bypass-logger.js +129 -0
  14. package/server/lib/code-gate/bypass-logger.test.js +142 -0
  15. package/server/lib/code-gate/gate-command.js +114 -0
  16. package/server/lib/code-gate/gate-command.test.js +111 -0
  17. package/server/lib/code-gate/gate-config.js +163 -0
  18. package/server/lib/code-gate/gate-config.test.js +181 -0
  19. package/server/lib/code-gate/gate-engine.js +193 -0
  20. package/server/lib/code-gate/gate-engine.test.js +258 -0
  21. package/server/lib/code-gate/gate-reporter.js +123 -0
  22. package/server/lib/code-gate/gate-reporter.test.js +159 -0
  23. package/server/lib/code-gate/hooks-generator.js +149 -0
  24. package/server/lib/code-gate/hooks-generator.test.js +142 -0
  25. package/server/lib/code-gate/llm-reviewer.js +176 -0
  26. package/server/lib/code-gate/llm-reviewer.test.js +161 -0
  27. package/server/lib/code-gate/push-gate.js +133 -0
  28. package/server/lib/code-gate/push-gate.test.js +190 -0
  29. package/server/lib/code-gate/rules/architecture-rules.js +228 -0
  30. package/server/lib/code-gate/rules/architecture-rules.test.js +155 -0
  31. package/server/lib/code-gate/rules/client-rules.js +120 -0
  32. package/server/lib/code-gate/rules/client-rules.test.js +121 -0
  33. package/server/lib/code-gate/rules/config-rules.js +140 -0
  34. package/server/lib/code-gate/rules/config-rules.test.js +103 -0
  35. package/server/lib/code-gate/rules/database-rules.js +158 -0
  36. package/server/lib/code-gate/rules/database-rules.test.js +119 -0
  37. package/server/lib/code-gate/rules/docker-rules.js +201 -0
  38. package/server/lib/code-gate/rules/docker-rules.test.js +104 -0
  39. package/server/lib/code-gate/rules/quality-rules.js +304 -0
  40. package/server/lib/code-gate/rules/quality-rules.test.js +199 -0
  41. package/server/lib/code-gate/rules/security-rules.js +228 -0
  42. package/server/lib/code-gate/rules/security-rules.test.js +131 -0
  43. package/server/lib/code-gate/rules/structure-rules.js +155 -0
  44. package/server/lib/code-gate/rules/structure-rules.test.js +107 -0
  45. package/server/lib/code-gate/rules/test-rules.js +93 -0
  46. package/server/lib/code-gate/rules/test-rules.test.js +97 -0
  47. package/server/lib/code-gate/typescript-gate.js +128 -0
  48. package/server/lib/code-gate/typescript-gate.test.js +131 -0
  49. package/server/lib/code-generator.test.js +1 -1
  50. package/server/lib/cost-command.test.js +1 -1
  51. package/server/lib/cost-optimizer.test.js +1 -1
  52. package/server/lib/cost-projections.test.js +1 -1
  53. package/server/lib/cost-reports.test.js +1 -1
  54. package/server/lib/cost-tracker.test.js +1 -1
  55. package/server/lib/crypto-patterns.test.js +1 -1
  56. package/server/lib/design-command.test.js +1 -1
  57. package/server/lib/design-parser.test.js +1 -1
  58. package/server/lib/gemini-vision.test.js +1 -1
  59. package/server/lib/input-validator.test.js +1 -1
  60. package/server/lib/litellm-client.test.js +1 -1
  61. package/server/lib/litellm-command.test.js +1 -1
  62. package/server/lib/litellm-config.test.js +1 -1
  63. package/server/lib/model-pricing.test.js +1 -1
  64. package/server/lib/models-command.test.js +1 -1
  65. package/server/lib/optimize-command.test.js +1 -1
  66. package/server/lib/orchestration-integration.test.js +1 -1
  67. package/server/lib/output-encoder.test.js +1 -1
  68. package/server/lib/quality-evaluator.test.js +1 -1
  69. package/server/lib/quality-gate-command.test.js +1 -1
  70. package/server/lib/quality-gate-scorer.test.js +1 -1
  71. package/server/lib/quality-history.test.js +1 -1
  72. package/server/lib/quality-presets.test.js +1 -1
  73. package/server/lib/quality-retry.test.js +1 -1
  74. package/server/lib/quality-thresholds.test.js +1 -1
  75. package/server/lib/secure-auth.test.js +1 -1
  76. package/server/lib/secure-code-command.test.js +1 -1
  77. package/server/lib/secure-errors.test.js +1 -1
  78. package/server/lib/security/auth-security.test.js +4 -3
  79. package/server/lib/vision-command.test.js +1 -1
  80. package/server/lib/visual-command.test.js +1 -1
  81. 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
+ });