clckernel 1.2.6
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/AGENTS.md +124 -0
- package/README.es.md +166 -0
- package/README.md +167 -0
- package/bin/cli.js +12 -0
- package/docs/Journal/001-adr-clckernel-governance.md +18 -0
- package/package.json +44 -0
- package/src/adapters/astro.js +67 -0
- package/src/adapters/base.js +109 -0
- package/src/adapters/django.js +70 -0
- package/src/adapters/fastapi.js +69 -0
- package/src/adapters/go.js +50 -0
- package/src/adapters/laravel.js +52 -0
- package/src/adapters/nextjs.js +72 -0
- package/src/adapters/rails.js +54 -0
- package/src/adapters/rust.js +51 -0
- package/src/catalog.js +217 -0
- package/src/config.js +250 -0
- package/src/detector.js +85 -0
- package/src/doctor.js +125 -0
- package/src/generator.js +473 -0
- package/src/index.js +52 -0
- package/src/technologies/detector.js +243 -0
- package/src/technologies/guards/_shared.js +66 -0
- package/src/ui.js +101 -0
- package/src/yaml.js +173 -0
- package/test/adapters/astro.test.js +102 -0
- package/test/adapters/base.test.js +86 -0
- package/test/adapters/django.test.js +95 -0
- package/test/adapters/fastapi.test.js +88 -0
- package/test/adapters/go.test.js +81 -0
- package/test/adapters/laravel.test.js +106 -0
- package/test/adapters/nextjs.test.js +113 -0
- package/test/adapters/rails.test.js +108 -0
- package/test/adapters/rust.test.js +83 -0
- package/test/catalog.test.js +170 -0
- package/test/config.test.js +512 -0
- package/test/detector.test.js +345 -0
- package/test/doctor.test.js +302 -0
- package/test/generator.test.js +419 -0
- package/test/guards_new.test.js +95 -0
- package/test/helpers.js +36 -0
- package/test/integration/cli-flow.test.js +270 -0
- package/test/technologies/celery_guard.test.js +185 -0
- package/test/technologies/detector.test.js +342 -0
- package/test/technologies/docker_guard.test.js +187 -0
- package/test/technologies/integration.test.js +123 -0
- package/test/technologies/postgres_guard.test.js +87 -0
- package/test/technologies/redis_guard.test.js +128 -0
- package/tools/audit.js +219 -0
- package/tools/audit.py +273 -0
- package/tools/celery_guard.py +209 -0
- package/tools/check_a11y.js +109 -0
- package/tools/check_api_contracts.js +139 -0
- package/tools/check_architecture.js +139 -0
- package/tools/check_architecture.py +264 -0
- package/tools/check_custom.js +163 -0
- package/tools/check_custom.py +395 -0
- package/tools/check_db_efficiency.py +190 -0
- package/tools/check_migrations.py +223 -0
- package/tools/check_performance.js +142 -0
- package/tools/check_responsive.js +131 -0
- package/tools/check_scope.py +180 -0
- package/tools/check_seo.js +139 -0
- package/tools/check_storybook.js +108 -0
- package/tools/check_ui_reuse.js +135 -0
- package/tools/docker_guard.py +210 -0
- package/tools/postgres_guard.py +192 -0
- package/tools/redis_guard.py +197 -0
- package/tools/scan_secrets.js +109 -0
- package/tools/scan_secrets.py +153 -0
- package/tools/verify_tdd.py +212 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
const { describe, it } = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { createTmpProject, teardown } = require('./helpers');
|
|
6
|
+
|
|
7
|
+
const { generateHookFromManifest, generateHarness } = require('../src/generator');
|
|
8
|
+
|
|
9
|
+
describe('generateHookFromManifest', () => {
|
|
10
|
+
describe('frontend hook', () => {
|
|
11
|
+
it('creates .husky/pre-commit with shebang and husky.sh line', () => {
|
|
12
|
+
const adapter = {
|
|
13
|
+
projectType: 'frontend',
|
|
14
|
+
name: 'Next.js (App Router)',
|
|
15
|
+
getTools: () => [
|
|
16
|
+
{ name: 'scan', command: 'node tools/scan_secrets.js', language: 'js', path: 'scan_secrets.js' },
|
|
17
|
+
{ name: 'tsc', command: 'npx tsc --noEmit', language: 'external', path: '' },
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
const dir = createTmpProject({});
|
|
21
|
+
try {
|
|
22
|
+
generateHookFromManifest(adapter, dir);
|
|
23
|
+
|
|
24
|
+
const hookFile = path.join(dir, '.husky', 'pre-commit');
|
|
25
|
+
assert.ok(fs.existsSync(hookFile), '.husky/pre-commit should exist');
|
|
26
|
+
|
|
27
|
+
const content = fs.readFileSync(hookFile, 'utf-8');
|
|
28
|
+
assert.ok(content.startsWith('#!/usr/bin/env sh'), 'should start with sh shebang');
|
|
29
|
+
assert.ok(content.includes('husky.sh'), 'should include husky.sh reference');
|
|
30
|
+
assert.ok(content.includes('node tools/scan_secrets.js'), 'should include tool command');
|
|
31
|
+
assert.ok(content.includes('npx tsc --noEmit'), 'should include external tool command');
|
|
32
|
+
|
|
33
|
+
// Frontend should NOT have .githooks directory
|
|
34
|
+
assert.ok(!fs.existsSync(path.join(dir, '.githooks')), 'should not create .githooks for frontend');
|
|
35
|
+
} finally {
|
|
36
|
+
teardown(dir);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('creates hook directory recursively', () => {
|
|
41
|
+
const adapter = {
|
|
42
|
+
projectType: 'frontend',
|
|
43
|
+
name: 'Astro Framework',
|
|
44
|
+
getTools: () => [],
|
|
45
|
+
};
|
|
46
|
+
const dir = createTmpProject({});
|
|
47
|
+
try {
|
|
48
|
+
generateHookFromManifest(adapter, dir);
|
|
49
|
+
assert.ok(fs.existsSync(path.join(dir, '.husky', 'pre-commit')), '.husky/pre-commit should exist');
|
|
50
|
+
} finally {
|
|
51
|
+
teardown(dir);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('backend hook', () => {
|
|
57
|
+
it('creates .githooks/pre-commit with bash shebang', () => {
|
|
58
|
+
const adapter = {
|
|
59
|
+
projectType: 'backend',
|
|
60
|
+
name: 'Django Framework',
|
|
61
|
+
getTools: () => [
|
|
62
|
+
{ name: 'scan', command: 'python3 tools/scan_secrets.py', language: 'py', path: 'scan_secrets.py' },
|
|
63
|
+
{ name: 'ruff', command: 'python3 -m ruff check .', language: 'external', path: '' },
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
const dir = createTmpProject({});
|
|
67
|
+
try {
|
|
68
|
+
generateHookFromManifest(adapter, dir);
|
|
69
|
+
|
|
70
|
+
const hookFile = path.join(dir, '.githooks', 'pre-commit');
|
|
71
|
+
assert.ok(fs.existsSync(hookFile), '.githooks/pre-commit should exist');
|
|
72
|
+
|
|
73
|
+
const content = fs.readFileSync(hookFile, 'utf-8');
|
|
74
|
+
assert.ok(content.startsWith('#!/usr/bin/env bash'), 'should start with bash shebang');
|
|
75
|
+
assert.ok(content.includes('Django Framework'), 'should mention adapter name');
|
|
76
|
+
assert.ok(content.includes('python3 tools/scan_secrets.py'), 'should include tool command');
|
|
77
|
+
assert.ok(content.includes('python3 -m ruff check .'), 'should include external tool command');
|
|
78
|
+
|
|
79
|
+
// Backend should NOT have .husky directory
|
|
80
|
+
assert.ok(!fs.existsSync(path.join(dir, '.husky')), 'should not create .husky for backend');
|
|
81
|
+
} finally {
|
|
82
|
+
teardown(dir);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('hooks directory is created recursively', () => {
|
|
87
|
+
const adapter = {
|
|
88
|
+
projectType: 'backend',
|
|
89
|
+
name: 'FastAPI Framework',
|
|
90
|
+
getTools: () => [],
|
|
91
|
+
};
|
|
92
|
+
const dir = createTmpProject({});
|
|
93
|
+
try {
|
|
94
|
+
generateHookFromManifest(adapter, dir);
|
|
95
|
+
assert.ok(fs.existsSync(path.join(dir, '.githooks', 'pre-commit')), '.githooks/pre-commit should exist');
|
|
96
|
+
} finally {
|
|
97
|
+
teardown(dir);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('skips tools without command', () => {
|
|
103
|
+
it('does not include tools with undefined command in hook', () => {
|
|
104
|
+
const adapter = {
|
|
105
|
+
projectType: 'frontend',
|
|
106
|
+
name: 'Next.js (App Router)',
|
|
107
|
+
getTools: () => [
|
|
108
|
+
{ name: 'scan', command: 'node tools/scan_secrets.js', language: 'js', path: 'scan_secrets.js' },
|
|
109
|
+
{ name: 'tsc', command: undefined, language: 'external', path: '' },
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
const dir = createTmpProject({});
|
|
113
|
+
try {
|
|
114
|
+
generateHookFromManifest(adapter, dir);
|
|
115
|
+
|
|
116
|
+
const hookFile = path.join(dir, '.husky', 'pre-commit');
|
|
117
|
+
const content = fs.readFileSync(hookFile, 'utf-8');
|
|
118
|
+
assert.ok(!content.includes('undefined'), 'should not include undefined in hook');
|
|
119
|
+
assert.ok(content.includes('node tools/scan_secrets.js'), 'should include valid command');
|
|
120
|
+
} finally {
|
|
121
|
+
teardown(dir);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('does not include tools with empty string command', () => {
|
|
126
|
+
const adapter = {
|
|
127
|
+
projectType: 'backend',
|
|
128
|
+
name: 'FastAPI Framework',
|
|
129
|
+
getTools: () => [
|
|
130
|
+
{ name: 'lint', command: '', language: 'external', path: '' },
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
const dir = createTmpProject({});
|
|
134
|
+
try {
|
|
135
|
+
generateHookFromManifest(adapter, dir);
|
|
136
|
+
|
|
137
|
+
const hookFile = path.join(dir, '.githooks', 'pre-commit');
|
|
138
|
+
const content = fs.readFileSync(hookFile, 'utf-8');
|
|
139
|
+
// Should only have shebang + echo + blank line, no empty command
|
|
140
|
+
const lines = content.split('\n').filter(l => l.trim().length > 0);
|
|
141
|
+
assert.strictEqual(lines.length, 2, 'should only have shebang and echo line');
|
|
142
|
+
} finally {
|
|
143
|
+
teardown(dir);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('generates empty hook when all tools lack commands', () => {
|
|
148
|
+
const adapter = {
|
|
149
|
+
projectType: 'frontend',
|
|
150
|
+
name: 'Next.js (App Router)',
|
|
151
|
+
getTools: () => [
|
|
152
|
+
{ name: 'tsc', command: undefined, language: 'external', path: '' },
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
const dir = createTmpProject({});
|
|
156
|
+
try {
|
|
157
|
+
generateHookFromManifest(adapter, dir);
|
|
158
|
+
|
|
159
|
+
const hookFile = path.join(dir, '.husky', 'pre-commit');
|
|
160
|
+
const content = fs.readFileSync(hookFile, 'utf-8');
|
|
161
|
+
// Just shebang + husky.sh line
|
|
162
|
+
assert.ok(content.startsWith('#!/usr/bin/env sh'));
|
|
163
|
+
assert.ok(content.includes('husky.sh'));
|
|
164
|
+
assert.ok(!content.includes('tsc'));
|
|
165
|
+
} finally {
|
|
166
|
+
teardown(dir);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('generateHarness', () => {
|
|
173
|
+
describe('with adapter', () => {
|
|
174
|
+
it('calls adapter.provision() and generates hook from manifest', () => {
|
|
175
|
+
let provisionCalled = false;
|
|
176
|
+
let provisionDir = null;
|
|
177
|
+
const adapter = {
|
|
178
|
+
projectType: 'frontend',
|
|
179
|
+
name: 'Next.js (App Router)',
|
|
180
|
+
provision(dir, config) {
|
|
181
|
+
provisionCalled = true;
|
|
182
|
+
provisionDir = dir;
|
|
183
|
+
// Simulate what BaseAdapter.provision does
|
|
184
|
+
fs.mkdirSync(path.join(dir, 'docs', 'Journal'), { recursive: true });
|
|
185
|
+
fs.mkdirSync(path.join(dir, 'docs', 'Architecture'), { recursive: true });
|
|
186
|
+
fs.mkdirSync(path.join(dir, 'docs', 'Development'), { recursive: true });
|
|
187
|
+
fs.mkdirSync(path.join(dir, 'sdds'), { recursive: true });
|
|
188
|
+
fs.mkdirSync(path.join(dir, 'tools'), { recursive: true });
|
|
189
|
+
fs.writeFileSync(path.join(dir, '.gitignore'), 'node_modules/\n', 'utf-8');
|
|
190
|
+
fs.writeFileSync(path.join(dir, 'AGENTS.md'), '# CLC Forge Next.js (App Router)');
|
|
191
|
+
},
|
|
192
|
+
getTools: () => [
|
|
193
|
+
{ name: 'scan', language: 'js', path: 'scan_secrets.js', command: 'node tools/scan_secrets.js' },
|
|
194
|
+
],
|
|
195
|
+
getSafeguards: () => ['Guard 1', 'Guard 2'],
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const config = {
|
|
199
|
+
projectType: 'frontend',
|
|
200
|
+
framework: 'Next.js (App Router)',
|
|
201
|
+
testRunner: 'Vitest / Jest',
|
|
202
|
+
adapter,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const dir = createTmpProject({});
|
|
206
|
+
try {
|
|
207
|
+
generateHarness(dir, config);
|
|
208
|
+
|
|
209
|
+
assert.ok(provisionCalled, 'adapter.provision() should be called');
|
|
210
|
+
assert.strictEqual(provisionDir, dir, 'provision should receive targetDir');
|
|
211
|
+
assert.ok(fs.existsSync(path.join(dir, 'docs', 'Journal')), 'docs/Journal should exist');
|
|
212
|
+
assert.ok(fs.existsSync(path.join(dir, 'sdds')), 'sdds/ should exist');
|
|
213
|
+
assert.ok(fs.existsSync(path.join(dir, 'tools')), 'tools/ should exist');
|
|
214
|
+
assert.ok(fs.existsSync(path.join(dir, 'AGENTS.md')), 'AGENTS.md should exist');
|
|
215
|
+
assert.ok(fs.existsSync(path.join(dir, '.husky', 'pre-commit')), '.husky/pre-commit should exist');
|
|
216
|
+
} finally {
|
|
217
|
+
teardown(dir);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('fallback (no adapter)', () => {
|
|
223
|
+
it('creates directory structure for frontend project', () => {
|
|
224
|
+
const config = {
|
|
225
|
+
projectType: 'frontend',
|
|
226
|
+
framework: 'Next.js',
|
|
227
|
+
testRunner: 'Vitest',
|
|
228
|
+
adapter: null,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const dir = createTmpProject({});
|
|
232
|
+
try {
|
|
233
|
+
generateHarness(dir, config);
|
|
234
|
+
|
|
235
|
+
assert.ok(fs.existsSync(path.join(dir, 'docs', 'Journal')), 'docs/Journal should exist');
|
|
236
|
+
assert.ok(fs.existsSync(path.join(dir, 'docs', 'Architecture')), 'docs/Architecture should exist');
|
|
237
|
+
assert.ok(fs.existsSync(path.join(dir, 'docs', 'Development')), 'docs/Development should exist');
|
|
238
|
+
assert.ok(fs.existsSync(path.join(dir, 'sdds')), 'sdds/ should exist');
|
|
239
|
+
assert.ok(fs.existsSync(path.join(dir, 'tools')), 'tools/ should exist');
|
|
240
|
+
} finally {
|
|
241
|
+
teardown(dir);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('creates AGENTS.md with framework and project type', () => {
|
|
246
|
+
const config = {
|
|
247
|
+
projectType: 'frontend',
|
|
248
|
+
framework: 'Next.js',
|
|
249
|
+
testRunner: 'Vitest',
|
|
250
|
+
adapter: null,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const dir = createTmpProject({});
|
|
254
|
+
try {
|
|
255
|
+
generateHarness(dir, config);
|
|
256
|
+
|
|
257
|
+
const agents = fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf-8');
|
|
258
|
+
assert.ok(agents.includes('Next.js'), 'AGENTS.md should include framework name');
|
|
259
|
+
assert.ok(agents.includes('FRONTEND'), 'AGENTS.md should include project type');
|
|
260
|
+
assert.ok(agents.includes('Vitest'), 'AGENTS.md should include test runner');
|
|
261
|
+
} finally {
|
|
262
|
+
teardown(dir);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('creates AGENTS.md with backend safeguards for backend project', () => {
|
|
267
|
+
const config = {
|
|
268
|
+
projectType: 'backend',
|
|
269
|
+
framework: 'FastAPI',
|
|
270
|
+
testRunner: 'Pytest',
|
|
271
|
+
adapter: null,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const dir = createTmpProject({});
|
|
275
|
+
try {
|
|
276
|
+
generateHarness(dir, config);
|
|
277
|
+
|
|
278
|
+
const agents = fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf-8');
|
|
279
|
+
assert.ok(agents.includes('FastAPI'), 'AGENTS.md should include framework name');
|
|
280
|
+
assert.ok(agents.includes('BACKEND'), 'AGENTS.md should include project type');
|
|
281
|
+
assert.ok(agents.includes('Scope Guardrail'), 'should include backend safeguard');
|
|
282
|
+
assert.ok(!agents.includes('UI Component Reuse First'), 'should not include frontend safeguards');
|
|
283
|
+
} finally {
|
|
284
|
+
teardown(dir);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('creates AGENTS.md with frontend safeguards for frontend project', () => {
|
|
289
|
+
const config = {
|
|
290
|
+
projectType: 'frontend',
|
|
291
|
+
framework: 'Next.js',
|
|
292
|
+
testRunner: 'Vitest',
|
|
293
|
+
adapter: null,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const dir = createTmpProject({});
|
|
297
|
+
try {
|
|
298
|
+
generateHarness(dir, config);
|
|
299
|
+
|
|
300
|
+
const agents = fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf-8');
|
|
301
|
+
assert.ok(agents.includes('UI Component Reuse First'), 'should include frontend safeguards');
|
|
302
|
+
assert.ok(agents.includes('Accessibility & ARIA Guard'), 'should include a11y guard');
|
|
303
|
+
assert.ok(!agents.includes('Scope Guardrail'), 'should not include backend safeguards');
|
|
304
|
+
} finally {
|
|
305
|
+
teardown(dir);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('writes .gitignore with sdds/* entry', () => {
|
|
310
|
+
const config = {
|
|
311
|
+
projectType: 'backend',
|
|
312
|
+
framework: 'FastAPI',
|
|
313
|
+
testRunner: 'Pytest',
|
|
314
|
+
adapter: null,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const dir = createTmpProject({});
|
|
318
|
+
try {
|
|
319
|
+
generateHarness(dir, config);
|
|
320
|
+
|
|
321
|
+
const gitignore = fs.readFileSync(path.join(dir, '.gitignore'), 'utf-8');
|
|
322
|
+
assert.ok(gitignore.includes('sdds/*'), '.gitignore should include sdds/*');
|
|
323
|
+
assert.ok(gitignore.includes('.gitkeep'), '.gitignore should reference .gitkeep');
|
|
324
|
+
} finally {
|
|
325
|
+
teardown(dir);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('creates .gitkeep in sdds/', () => {
|
|
330
|
+
const config = {
|
|
331
|
+
projectType: 'backend',
|
|
332
|
+
framework: 'FastAPI',
|
|
333
|
+
testRunner: 'Pytest',
|
|
334
|
+
adapter: null,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const dir = createTmpProject({});
|
|
338
|
+
try {
|
|
339
|
+
generateHarness(dir, config);
|
|
340
|
+
assert.ok(fs.existsSync(path.join(dir, 'sdds', '.gitkeep')), 'sdds/.gitkeep should exist');
|
|
341
|
+
} finally {
|
|
342
|
+
teardown(dir);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('appends to existing .gitignore without duplicating sdds/*', () => {
|
|
347
|
+
const config = {
|
|
348
|
+
projectType: 'backend',
|
|
349
|
+
framework: 'FastAPI',
|
|
350
|
+
testRunner: 'Pytest',
|
|
351
|
+
adapter: null,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const dir = createTmpProject({
|
|
355
|
+
'.gitignore': 'node_modules/\n.env\n',
|
|
356
|
+
});
|
|
357
|
+
try {
|
|
358
|
+
generateHarness(dir, config);
|
|
359
|
+
|
|
360
|
+
const gitignore = fs.readFileSync(path.join(dir, '.gitignore'), 'utf-8');
|
|
361
|
+
const matches = gitignore.match(/sdds\/\*/g);
|
|
362
|
+
assert.strictEqual(matches.length, 1, 'sdds/* should appear exactly once');
|
|
363
|
+
assert.ok(gitignore.includes('node_modules/'), 'original content preserved');
|
|
364
|
+
} finally {
|
|
365
|
+
teardown(dir);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe('fallback hook generation', () => {
|
|
371
|
+
it('creates .husky/pre-commit for frontend projects', () => {
|
|
372
|
+
const config = {
|
|
373
|
+
projectType: 'frontend',
|
|
374
|
+
framework: 'Next.js',
|
|
375
|
+
testRunner: 'Vitest',
|
|
376
|
+
adapter: null,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const dir = createTmpProject({});
|
|
380
|
+
try {
|
|
381
|
+
generateHarness(dir, config);
|
|
382
|
+
|
|
383
|
+
const hookFile = path.join(dir, '.husky', 'pre-commit');
|
|
384
|
+
assert.ok(fs.existsSync(hookFile), '.husky/pre-commit should exist');
|
|
385
|
+
|
|
386
|
+
const content = fs.readFileSync(hookFile, 'utf-8');
|
|
387
|
+
assert.ok(content.startsWith('#!/usr/bin/env sh'), 'should have sh shebang');
|
|
388
|
+
assert.ok(content.includes('husky.sh'), 'should include husky.sh');
|
|
389
|
+
assert.ok(content.includes('scan_secrets'), 'should reference scan_secrets');
|
|
390
|
+
} finally {
|
|
391
|
+
teardown(dir);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('creates .githooks/pre-commit for backend projects', () => {
|
|
396
|
+
const config = {
|
|
397
|
+
projectType: 'backend',
|
|
398
|
+
framework: 'FastAPI',
|
|
399
|
+
testRunner: 'Pytest',
|
|
400
|
+
adapter: null,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const dir = createTmpProject({});
|
|
404
|
+
try {
|
|
405
|
+
generateHarness(dir, config);
|
|
406
|
+
|
|
407
|
+
const hookFile = path.join(dir, '.githooks', 'pre-commit');
|
|
408
|
+
assert.ok(fs.existsSync(hookFile), '.githooks/pre-commit should exist');
|
|
409
|
+
|
|
410
|
+
const content = fs.readFileSync(hookFile, 'utf-8');
|
|
411
|
+
assert.ok(content.startsWith('#!/usr/bin/env bash'), 'should have bash shebang');
|
|
412
|
+
assert.ok(content.includes('CLC Kernel') || content.includes('CLC Forge'), 'should include CLC branding');
|
|
413
|
+
assert.ok(content.includes('scan_secrets'), 'should reference scan_secrets');
|
|
414
|
+
} finally {
|
|
415
|
+
teardown(dir);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const { describe, it } = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { execSync, spawnSync } = require('child_process');
|
|
6
|
+
const { createTmpProject, teardown } = require('./helpers');
|
|
7
|
+
|
|
8
|
+
function initGitRepo(dir) {
|
|
9
|
+
fs.writeFileSync(path.join(dir, '.gitkeep'), '');
|
|
10
|
+
execSync('git init && git config user.name "Test" && git config user.email "test@example.com" && git add . && git commit -m "initial"', { cwd: dir });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('New Automatic Safeguards', () => {
|
|
14
|
+
const checkResponsive = require('../tools/check_responsive');
|
|
15
|
+
const checkSeo = require('../tools/check_seo');
|
|
16
|
+
|
|
17
|
+
describe('check_responsive.js', () => {
|
|
18
|
+
it('returns exitCode 0 when no git diff is available (graceful degradation)', async () => {
|
|
19
|
+
const dir = createTmpProject({});
|
|
20
|
+
try {
|
|
21
|
+
const res = await checkResponsive(dir);
|
|
22
|
+
assert.strictEqual(res.exitCode, 0);
|
|
23
|
+
assert.strictEqual(res.name, 'check_responsive');
|
|
24
|
+
} finally {
|
|
25
|
+
teardown(dir);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('detects hardcoded fixed desktop width in git diff', async () => {
|
|
30
|
+
const dir = createTmpProject({});
|
|
31
|
+
initGitRepo(dir);
|
|
32
|
+
try {
|
|
33
|
+
fs.writeFileSync(path.join(dir, 'component.tsx'), '<div className="w-[1200px]">Fixed</div>');
|
|
34
|
+
execSync('git add component.tsx', { cwd: dir });
|
|
35
|
+
|
|
36
|
+
const res = await checkResponsive(dir);
|
|
37
|
+
assert.strictEqual(res.exitCode, 1);
|
|
38
|
+
assert.ok(res.data.violations['component.tsx']);
|
|
39
|
+
assert.strictEqual(res.data.violations['component.tsx'][0].rule, 'responsive-fixed-width');
|
|
40
|
+
} finally {
|
|
41
|
+
teardown(dir);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('check_seo.js', () => {
|
|
47
|
+
it('returns exitCode 0 when no git diff is available (graceful degradation)', async () => {
|
|
48
|
+
const dir = createTmpProject({});
|
|
49
|
+
try {
|
|
50
|
+
const res = await checkSeo(dir);
|
|
51
|
+
assert.strictEqual(res.exitCode, 0);
|
|
52
|
+
assert.strictEqual(res.name, 'check_seo');
|
|
53
|
+
} finally {
|
|
54
|
+
teardown(dir);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('detects multiple <h1> tags in single page component', async () => {
|
|
59
|
+
const dir = createTmpProject({});
|
|
60
|
+
initGitRepo(dir);
|
|
61
|
+
try {
|
|
62
|
+
fs.writeFileSync(path.join(dir, 'page.tsx'), 'export default function Page() { return <div><h1>One</h1><h1>Two</h1></div>; }');
|
|
63
|
+
execSync('git add page.tsx', { cwd: dir });
|
|
64
|
+
|
|
65
|
+
const res = await checkSeo(dir);
|
|
66
|
+
assert.strictEqual(res.exitCode, 1);
|
|
67
|
+
assert.ok(res.data.violations['page.tsx']);
|
|
68
|
+
assert.strictEqual(res.data.violations['page.tsx'][0].rule, 'seo-multiple-h1');
|
|
69
|
+
} finally {
|
|
70
|
+
teardown(dir);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('check_db_efficiency.py', () => {
|
|
76
|
+
it('detects database query inside for-loop via python AST', () => {
|
|
77
|
+
const dir = createTmpProject({});
|
|
78
|
+
initGitRepo(dir);
|
|
79
|
+
try {
|
|
80
|
+
fs.writeFileSync(path.join(dir, 'service.py'), 'def bad_loop(ids):\n res = []\n for i in ids:\n res.append(db.query(User).filter(User.id == i).first())\n return res\n');
|
|
81
|
+
execSync('git add service.py', { cwd: dir });
|
|
82
|
+
|
|
83
|
+
const scriptPath = path.resolve(__dirname, '../tools/check_db_efficiency.py');
|
|
84
|
+
const proc = spawnSync('python3', [scriptPath, dir], { cwd: dir, encoding: 'utf-8' });
|
|
85
|
+
assert.strictEqual(proc.status, 1);
|
|
86
|
+
const parsed = JSON.parse(proc.stdout);
|
|
87
|
+
assert.strictEqual(parsed.exit_code, 1);
|
|
88
|
+
assert.ok(parsed.data.violations['service.py']);
|
|
89
|
+
assert.strictEqual(parsed.data.violations['service.py'][0].rule, 'db-query-in-loop');
|
|
90
|
+
} finally {
|
|
91
|
+
teardown(dir);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
package/test/helpers.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test utilities for CLC Forge test suite.
|
|
3
|
+
* Provides temp directory creation, project scaffolding, and teardown.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a temporary project directory with the given file structure.
|
|
12
|
+
* Returns the path to the temp directory.
|
|
13
|
+
*
|
|
14
|
+
* @param {Record<string, string>} files - Map of relative paths to file contents.
|
|
15
|
+
* @returns {string} Path to the created temp directory.
|
|
16
|
+
*/
|
|
17
|
+
function createTmpProject(files = {}) {
|
|
18
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'clc-forge-test-'));
|
|
19
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
20
|
+
const filePath = path.join(dir, rel);
|
|
21
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
22
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Recursively remove a temporary directory.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} dir - Path to remove.
|
|
31
|
+
*/
|
|
32
|
+
function teardown(dir) {
|
|
33
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { createTmpProject, teardown };
|