sumulige-claude 1.1.2 → 1.2.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/.claude/hooks/code-formatter.cjs +7 -2
- package/.claude/hooks/multi-session.cjs +9 -3
- package/.claude/hooks/pre-commit.cjs +0 -0
- package/.claude/hooks/pre-push.cjs +0 -0
- package/.claude/hooks/project-kickoff.cjs +22 -11
- package/.claude/hooks/rag-skill-loader.cjs +7 -0
- package/.claude/hooks/thinking-silent.cjs +9 -3
- package/.claude/hooks/todo-manager.cjs +19 -13
- package/.claude/hooks/verify-work.cjs +10 -4
- package/.claude/quality-gate.json +9 -3
- package/.claude/settings.local.json +16 -1
- package/.claude/templates/hooks/README.md +302 -0
- package/.claude/templates/hooks/hook.sh.template +94 -0
- package/.claude/templates/hooks/user-prompt-submit.cjs.template +116 -0
- package/.claude/templates/hooks/user-response-submit.cjs.template +94 -0
- package/.claude/templates/hooks/validate.js +173 -0
- package/.claude/workflow/document-scanner.js +426 -0
- package/.claude/workflow/knowledge-engine.js +941 -0
- package/.claude/workflow/notebooklm/browser.js +1028 -0
- package/.claude/workflow/phases/phase1-research.js +578 -0
- package/.claude/workflow/phases/phase1-research.ts +465 -0
- package/.claude/workflow/phases/phase2-approve.js +722 -0
- package/.claude/workflow/phases/phase3-plan.js +1200 -0
- package/.claude/workflow/phases/phase4-develop.js +894 -0
- package/.claude/workflow/search-cache.js +230 -0
- package/.claude/workflow/templates/approval.md +315 -0
- package/.claude/workflow/templates/development.md +377 -0
- package/.claude/workflow/templates/planning.md +328 -0
- package/.claude/workflow/templates/research.md +250 -0
- package/.claude/workflow/types.js +37 -0
- package/.claude/workflow/web-search.js +278 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +176 -0
- package/CHANGELOG.md +7 -14
- package/cli.js +20 -0
- package/config/quality-gate.json +9 -3
- package/development/cache/web-search/search_1193d605f8eb364651fc2f2041b58a31.json +36 -0
- package/development/cache/web-search/search_3798bf06960edc125f744a1abb5b72c5.json +36 -0
- package/development/cache/web-search/search_37c7d4843a53f0d83f1122a6f908a2a3.json +36 -0
- package/development/cache/web-search/search_44166fa0153709ee168485a22aa0ab40.json +36 -0
- package/development/cache/web-search/search_4deaebb1f77e86a8ca066dc5a49c59fd.json +36 -0
- package/development/cache/web-search/search_94da91789466070a7f545612e73c7372.json +36 -0
- package/development/cache/web-search/search_dd5de8491b8b803a3cb01339cd210fb0.json +36 -0
- package/development/knowledge-base/.index.clean.json +0 -0
- package/development/knowledge-base/.index.json +486 -0
- package/development/knowledge-base/test-best-practices.md +29 -0
- package/development/projects/proj_mkh1pazz_ixmt1/phase1/feasibility-report.md +160 -0
- package/development/projects/proj_mkh4jvnb_z7rwf/phase1/feasibility-report.md +160 -0
- package/development/projects/proj_mkh4jxkd_ewz5a/phase1/feasibility-report.md +160 -0
- package/development/projects/proj_mkh4k84n_ni73k/phase1/feasibility-report.md +160 -0
- package/development/projects/proj_mkh4wfyd_u9w88/phase1/feasibility-report.md +160 -0
- package/development/projects/proj_mkh4wsbo_iahvf/development/projects/proj_mkh4xbpg_4na5w/phase1/feasibility-report.md +160 -0
- package/development/projects/proj_mkh4wsbo_iahvf/phase1/feasibility-report.md +160 -0
- package/development/projects/proj_mkh4xulg_1ka8x/phase1/feasibility-report.md +160 -0
- package/development/projects/proj_mkh4xwhj_gch8j/phase1/feasibility-report.md +160 -0
- package/development/projects/proj_mkh4y2qk_9lm8z/phase1/feasibility-report.md +160 -0
- package/development/projects/proj_mkh4y2qk_9lm8z/phase2/requirements.md +226 -0
- package/development/projects/proj_mkh4y2qk_9lm8z/phase3/PRD.md +345 -0
- package/development/projects/proj_mkh4y2qk_9lm8z/phase3/TASK_PLAN.md +284 -0
- package/development/projects/proj_mkh4y2qk_9lm8z/phase3/prototype/README.md +14 -0
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/DEVELOPMENT_LOG.md +35 -0
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/TASKS.md +34 -0
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/.env.example +5 -0
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/README.md +60 -0
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/package.json +25 -0
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/src/index.js +70 -0
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/src/routes/index.js +48 -0
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/tests/health.test.js +20 -0
- package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/tests/jest.config.js +21 -0
- package/development/projects/proj_mkh7veqg_3lypc/phase1/feasibility-report.md +160 -0
- package/development/projects/proj_mkh7veqg_3lypc/phase2/requirements.md +226 -0
- package/development/projects/proj_mkh7veqg_3lypc/phase3/PRD.md +345 -0
- package/development/projects/proj_mkh7veqg_3lypc/phase3/TASK_PLAN.md +284 -0
- package/development/projects/proj_mkh7veqg_3lypc/phase3/prototype/README.md +14 -0
- package/development/projects/proj_mkh8k8fo_rmqn5/phase1/feasibility-report.md +160 -0
- package/development/projects/proj_mkh8xyhy_1vshq/phase1/feasibility-report.md +178 -0
- package/development/projects/proj_mkh8zddd_dhamf/phase1/feasibility-report.md +377 -0
- package/development/projects/proj_mkh8zddd_dhamf/phase2/requirements.md +442 -0
- package/development/projects/proj_mkh8zddd_dhamf/phase3/api-design.md +800 -0
- package/development/projects/proj_mkh8zddd_dhamf/phase3/architecture.md +625 -0
- package/development/projects/proj_mkh8zddd_dhamf/phase3/data-model.md +830 -0
- package/development/projects/proj_mkh8zddd_dhamf/phase3/risks.md +957 -0
- package/development/projects/proj_mkh8zddd_dhamf/phase3/wbs.md +381 -0
- package/development/todos/.state.json +14 -1
- package/development/todos/INDEX.md +31 -73
- package/development/todos/completed/develop/local-knowledge-index.md +85 -0
- package/development/todos/{active → completed/develop}/todo-system.md +13 -3
- package/development/todos/completed/develop/web-search-integration.md +83 -0
- package/development/todos/completed/test/phase1-e2e-test.md +103 -0
- package/lib/commands.js +388 -0
- package/package.json +3 -2
- package/tests/config-manager.test.js +677 -0
- package/tests/config-validator.test.js +436 -0
- package/tests/errors.test.js +477 -0
- package/tests/manual/phase1-e2e.sh +389 -0
- package/tests/manual/phase2-test-cases.md +311 -0
- package/tests/manual/phase3-test-cases.md +309 -0
- package/tests/manual/phase4-test-cases.md +414 -0
- package/tests/manual/test-cases.md +417 -0
- package/tests/quality-gate.test.js +679 -0
- package/tests/quality-rules.test.js +619 -0
- package/tests/version-check.test.js +75 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Gate 模块单元测试
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
describe('Quality Gate Module', () => {
|
|
10
|
+
const {
|
|
11
|
+
QualityGate,
|
|
12
|
+
ConsoleReporter,
|
|
13
|
+
JsonReporter,
|
|
14
|
+
MarkdownReporter,
|
|
15
|
+
checkOrThrow
|
|
16
|
+
} = require('../lib/quality-gate');
|
|
17
|
+
|
|
18
|
+
const { QualityGateError } = require('../lib/errors');
|
|
19
|
+
|
|
20
|
+
// Temporary test directory
|
|
21
|
+
const tempDir = path.join(os.tmpdir(), 'smc-gate-test-' + Date.now());
|
|
22
|
+
const cleanJsFile = path.join(tempDir, 'clean.js');
|
|
23
|
+
const dirtyJsFile = path.join(tempDir, 'dirty.js');
|
|
24
|
+
const largeJsFile = path.join(tempDir, 'large.js');
|
|
25
|
+
const emptyJsFile = path.join(tempDir, 'empty.js');
|
|
26
|
+
const subDir = path.join(tempDir, 'sub');
|
|
27
|
+
const subDirFile = path.join(subDir, 'file.js');
|
|
28
|
+
|
|
29
|
+
beforeAll(() => {
|
|
30
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
31
|
+
fs.mkdirSync(subDir, { recursive: true });
|
|
32
|
+
|
|
33
|
+
// Create test files
|
|
34
|
+
fs.writeFileSync(cleanJsFile, 'const x = 1;\nconst y = 2;\n');
|
|
35
|
+
fs.writeFileSync(dirtyJsFile, 'const x = 1; \nconst y = 2;\t\n');
|
|
36
|
+
|
|
37
|
+
// Create large file
|
|
38
|
+
const lines = ['// Large file'];
|
|
39
|
+
for (let i = 0; i < 850; i++) {
|
|
40
|
+
lines.push(`const line${i} = ${i};`);
|
|
41
|
+
}
|
|
42
|
+
fs.writeFileSync(largeJsFile, lines.join('\n'));
|
|
43
|
+
|
|
44
|
+
fs.writeFileSync(emptyJsFile, '\n');
|
|
45
|
+
fs.writeFileSync(subDirFile, 'const x = 1;\n');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterAll(() => {
|
|
49
|
+
if (fs.existsSync(tempDir)) {
|
|
50
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('QualityGate', () => {
|
|
55
|
+
describe('constructor', () => {
|
|
56
|
+
it('should create gate with default options', () => {
|
|
57
|
+
const gate = new QualityGate();
|
|
58
|
+
|
|
59
|
+
expect(gate.projectDir).toBeDefined();
|
|
60
|
+
expect(gate.config).toBeDefined();
|
|
61
|
+
expect(gate.reporters).toBeDefined();
|
|
62
|
+
expect(gate.reporters.length).toBeGreaterThan(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should accept custom project directory', () => {
|
|
66
|
+
const gate = new QualityGate({ projectDir: tempDir });
|
|
67
|
+
|
|
68
|
+
expect(gate.projectDir).toBe(tempDir);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should accept custom config', () => {
|
|
72
|
+
const customConfig = {
|
|
73
|
+
enabled: true,
|
|
74
|
+
severity: 'error',
|
|
75
|
+
rules: []
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const gate = new QualityGate({ config: customConfig });
|
|
79
|
+
|
|
80
|
+
expect(gate.config).toEqual(customConfig);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should accept custom reporters', () => {
|
|
84
|
+
const customReporter = {
|
|
85
|
+
report: jest.fn()
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const gate = new QualityGate({ reporters: [customReporter] });
|
|
89
|
+
|
|
90
|
+
expect(gate.reporters).toHaveLength(1);
|
|
91
|
+
expect(gate.reporters[0]).toBe(customReporter);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('check', () => {
|
|
96
|
+
let gate;
|
|
97
|
+
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
gate = new QualityGate({
|
|
100
|
+
projectDir: tempDir,
|
|
101
|
+
reporters: [] // Suppress output during tests
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should return check result object', async () => {
|
|
106
|
+
const result = await gate.check();
|
|
107
|
+
|
|
108
|
+
expect(result).toBeDefined();
|
|
109
|
+
expect(typeof result.passed).toBe('boolean');
|
|
110
|
+
expect(Array.isArray(result.results)).toBe(true);
|
|
111
|
+
expect(result.summary).toBeDefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should scan project files', async () => {
|
|
115
|
+
const result = await gate.check();
|
|
116
|
+
|
|
117
|
+
expect(result.summary.filesChecked).toBeGreaterThan(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should run quality rules', async () => {
|
|
121
|
+
const result = await gate.check();
|
|
122
|
+
|
|
123
|
+
expect(result.summary.rulesRun).toBeGreaterThan(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should detect issues in files', async () => {
|
|
127
|
+
const result = await gate.check();
|
|
128
|
+
|
|
129
|
+
// Large file should trigger line-count-limit
|
|
130
|
+
const lineCountIssues = result.results.filter(r => r.rule === 'line-count-limit');
|
|
131
|
+
expect(lineCountIssues.length).toBeGreaterThan(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should count issues by severity', async () => {
|
|
135
|
+
const result = await gate.check();
|
|
136
|
+
|
|
137
|
+
expect(result.summary).toMatchObject({
|
|
138
|
+
total: expect.any(Number),
|
|
139
|
+
critical: expect.any(Number),
|
|
140
|
+
error: expect.any(Number),
|
|
141
|
+
warn: expect.any(Number),
|
|
142
|
+
info: expect.any(Number)
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should pass when severity is critical and no critical issues', async () => {
|
|
147
|
+
const result = await gate.check({ severity: 'critical' });
|
|
148
|
+
|
|
149
|
+
// Should pass if no critical issues
|
|
150
|
+
expect(typeof result.passed).toBe('boolean');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should fail when blocking issues exist', async () => {
|
|
154
|
+
// Use a file with known issues
|
|
155
|
+
const gateWithIssues = new QualityGate({
|
|
156
|
+
projectDir: tempDir,
|
|
157
|
+
reporters: [],
|
|
158
|
+
config: {
|
|
159
|
+
rules: [
|
|
160
|
+
{ id: 'line-count-limit', enabled: true, severity: 'error' }
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const result = await gateWithIssues.check({ severity: 'error' });
|
|
166
|
+
|
|
167
|
+
// Large file should cause error severity issue
|
|
168
|
+
if (result.summary.error > 0) {
|
|
169
|
+
expect(result.passed).toBe(false);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should respect severity threshold', async () => {
|
|
174
|
+
const resultWarn = await gate.check({ severity: 'warn' });
|
|
175
|
+
const resultCritical = await gate.check({ severity: 'critical' });
|
|
176
|
+
|
|
177
|
+
// Critical threshold is less strict than warn (fewer blocking issues)
|
|
178
|
+
// So critical should pass more often than warn
|
|
179
|
+
if (resultWarn.passed === false && resultCritical.passed === true) {
|
|
180
|
+
// This is the expected case: warn fails but critical passes
|
|
181
|
+
expect(true).toBe(true);
|
|
182
|
+
} else {
|
|
183
|
+
// If both pass or both fail, that's also acceptable
|
|
184
|
+
expect(resultCritical.passed).toBe(resultWarn.passed);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should check specific files when provided', async () => {
|
|
189
|
+
const result = await gate.check({ files: [cleanJsFile] });
|
|
190
|
+
|
|
191
|
+
expect(result.summary.filesChecked).toBe(1);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should run specific rules when provided', async () => {
|
|
195
|
+
const result = await gate.check({
|
|
196
|
+
rules: [{ id: 'no-empty-files', enabled: true }]
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(result.summary.rulesRun).toBe(1);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should apply auto-fixes when requested', async () => {
|
|
203
|
+
// Create a file with trailing whitespace for auto-fix test
|
|
204
|
+
const fixableFile = path.join(tempDir, 'fixable.js');
|
|
205
|
+
fs.writeFileSync(fixableFile, 'const x = 1; \nconst y = 2;\t\n');
|
|
206
|
+
|
|
207
|
+
const result = await gate.check({
|
|
208
|
+
files: [fixableFile],
|
|
209
|
+
fix: true
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// The file should now be fixed
|
|
213
|
+
const content = fs.readFileSync(fixableFile, 'utf-8');
|
|
214
|
+
const hasTrailing = content.match(/[ \t]$/gm);
|
|
215
|
+
expect(hasTrailing).toBeNull();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should track fixed issues in summary', async () => {
|
|
219
|
+
const fixableFile = path.join(tempDir, 'fixable2.js');
|
|
220
|
+
fs.writeFileSync(fixableFile, 'const x = 1; \n');
|
|
221
|
+
|
|
222
|
+
const result = await gate.check({
|
|
223
|
+
files: [fixableFile],
|
|
224
|
+
fix: true
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(result.summary).toHaveProperty('fixed');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('_getProjectFiles', () => {
|
|
232
|
+
it('should ignore common directories', async () => {
|
|
233
|
+
// Create node_modules directory
|
|
234
|
+
const nodeModulesDir = path.join(tempDir, 'node_modules');
|
|
235
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
236
|
+
fs.writeFileSync(path.join(nodeModulesDir, 'index.js'), 'ignored');
|
|
237
|
+
|
|
238
|
+
const gate = new QualityGate({ projectDir: tempDir, reporters: [] });
|
|
239
|
+
const result = await gate.check();
|
|
240
|
+
|
|
241
|
+
// Should not include node_modules files
|
|
242
|
+
const nodeModulesFiles = result.results.filter(r =>
|
|
243
|
+
r.file.includes('node_modules')
|
|
244
|
+
);
|
|
245
|
+
expect(nodeModulesFiles).toHaveLength(0);
|
|
246
|
+
|
|
247
|
+
fs.rmSync(nodeModulesDir, { recursive: true, force: true });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should check supported file extensions', async () => {
|
|
251
|
+
const gate = new QualityGate({ projectDir: tempDir, reporters: [] });
|
|
252
|
+
const result = await gate.check();
|
|
253
|
+
|
|
254
|
+
// Should have checked .js files
|
|
255
|
+
const jsFiles = result.results.filter(r => r.file.endsWith('.js'));
|
|
256
|
+
expect(jsFiles.length).toBeGreaterThan(0);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('_getActiveRules', () => {
|
|
261
|
+
it('should load rules from config file', () => {
|
|
262
|
+
const configDir = path.join(tempDir, '.claude');
|
|
263
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
264
|
+
|
|
265
|
+
fs.writeFileSync(
|
|
266
|
+
path.join(configDir, 'quality-gate.json'),
|
|
267
|
+
JSON.stringify({
|
|
268
|
+
enabled: true,
|
|
269
|
+
severity: 'warn',
|
|
270
|
+
rules: [
|
|
271
|
+
{ id: 'line-count-limit', enabled: true, severity: 'error' }
|
|
272
|
+
]
|
|
273
|
+
})
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const gate = new QualityGate({ projectDir: tempDir, reporters: [] });
|
|
277
|
+
const rules = gate._getActiveRules();
|
|
278
|
+
|
|
279
|
+
expect(rules.length).toBeGreaterThan(0);
|
|
280
|
+
|
|
281
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should use default config when file doesn\'t exist', () => {
|
|
285
|
+
const gate = new QualityGate({ projectDir: tempDir, reporters: [] });
|
|
286
|
+
const rules = gate._getActiveRules();
|
|
287
|
+
|
|
288
|
+
expect(rules.length).toBeGreaterThan(0);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('ConsoleReporter', () => {
|
|
294
|
+
let reporter;
|
|
295
|
+
let consoleSpy;
|
|
296
|
+
|
|
297
|
+
beforeEach(() => {
|
|
298
|
+
reporter = new ConsoleReporter();
|
|
299
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
afterEach(() => {
|
|
303
|
+
consoleSpy.mockRestore();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should report pass status', () => {
|
|
307
|
+
const result = {
|
|
308
|
+
passed: true,
|
|
309
|
+
summary: {
|
|
310
|
+
filesChecked: 10,
|
|
311
|
+
total: 0,
|
|
312
|
+
critical: 0,
|
|
313
|
+
error: 0,
|
|
314
|
+
warn: 0
|
|
315
|
+
},
|
|
316
|
+
results: []
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
reporter.report(result);
|
|
320
|
+
|
|
321
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
322
|
+
expect.stringContaining('PASS')
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should report fail status', () => {
|
|
327
|
+
const result = {
|
|
328
|
+
passed: false,
|
|
329
|
+
summary: {
|
|
330
|
+
filesChecked: 10,
|
|
331
|
+
total: 5,
|
|
332
|
+
critical: 1,
|
|
333
|
+
error: 2,
|
|
334
|
+
warn: 2
|
|
335
|
+
},
|
|
336
|
+
results: [
|
|
337
|
+
{
|
|
338
|
+
file: '/path/to/file.js',
|
|
339
|
+
ruleName: 'Test Rule',
|
|
340
|
+
severity: 'error',
|
|
341
|
+
message: 'Test error',
|
|
342
|
+
fix: 'Fix it'
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
reporter.report(result);
|
|
348
|
+
|
|
349
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
350
|
+
expect.stringContaining('FAIL')
|
|
351
|
+
);
|
|
352
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
353
|
+
expect.stringContaining('Test Rule')
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should display file paths', () => {
|
|
358
|
+
const result = {
|
|
359
|
+
passed: false,
|
|
360
|
+
summary: {
|
|
361
|
+
filesChecked: 1,
|
|
362
|
+
total: 1,
|
|
363
|
+
critical: 0,
|
|
364
|
+
error: 1,
|
|
365
|
+
warn: 0
|
|
366
|
+
},
|
|
367
|
+
results: [
|
|
368
|
+
{
|
|
369
|
+
file: path.join(tempDir, 'test.js'),
|
|
370
|
+
ruleName: 'Line Count',
|
|
371
|
+
severity: 'error',
|
|
372
|
+
message: 'Too many lines',
|
|
373
|
+
pass: false
|
|
374
|
+
}
|
|
375
|
+
]
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
reporter.report(result);
|
|
379
|
+
|
|
380
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
381
|
+
expect.stringContaining('test.js')
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should show fix suggestions', () => {
|
|
386
|
+
const result = {
|
|
387
|
+
passed: false,
|
|
388
|
+
summary: {
|
|
389
|
+
filesChecked: 1,
|
|
390
|
+
total: 1,
|
|
391
|
+
critical: 0,
|
|
392
|
+
error: 1,
|
|
393
|
+
warn: 0
|
|
394
|
+
},
|
|
395
|
+
results: [
|
|
396
|
+
{
|
|
397
|
+
file: '/path/to/file.js',
|
|
398
|
+
ruleName: 'Test Rule',
|
|
399
|
+
severity: 'warn',
|
|
400
|
+
message: 'Test warning',
|
|
401
|
+
pass: false,
|
|
402
|
+
fix: 'Apply this fix'
|
|
403
|
+
}
|
|
404
|
+
]
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
reporter.report(result);
|
|
408
|
+
|
|
409
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
410
|
+
expect.stringContaining('Fix:')
|
|
411
|
+
);
|
|
412
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
413
|
+
expect.stringContaining('Apply this fix')
|
|
414
|
+
);
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
describe('JsonReporter', () => {
|
|
419
|
+
let reporter;
|
|
420
|
+
let consoleSpy;
|
|
421
|
+
|
|
422
|
+
beforeEach(() => {
|
|
423
|
+
reporter = new JsonReporter();
|
|
424
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
afterEach(() => {
|
|
428
|
+
consoleSpy.mockRestore();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should output JSON', () => {
|
|
432
|
+
const result = {
|
|
433
|
+
passed: true,
|
|
434
|
+
summary: { filesChecked: 5, total: 0 },
|
|
435
|
+
results: []
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
reporter.report(result);
|
|
439
|
+
|
|
440
|
+
const output = consoleSpy.mock.calls[0][0];
|
|
441
|
+
|
|
442
|
+
expect(() => JSON.parse(output)).not.toThrow();
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('should include all result data', () => {
|
|
446
|
+
const result = {
|
|
447
|
+
passed: false,
|
|
448
|
+
summary: {
|
|
449
|
+
filesChecked: 1,
|
|
450
|
+
total: 1,
|
|
451
|
+
critical: 0,
|
|
452
|
+
error: 1,
|
|
453
|
+
warn: 0
|
|
454
|
+
},
|
|
455
|
+
results: [
|
|
456
|
+
{
|
|
457
|
+
file: '/test.js',
|
|
458
|
+
rule: 'test-rule',
|
|
459
|
+
ruleName: 'Test Rule',
|
|
460
|
+
severity: 'error',
|
|
461
|
+
message: 'Test error',
|
|
462
|
+
pass: false
|
|
463
|
+
}
|
|
464
|
+
]
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
reporter.report(result);
|
|
468
|
+
|
|
469
|
+
const output = JSON.parse(consoleSpy.mock.calls[0][0]);
|
|
470
|
+
|
|
471
|
+
expect(output.passed).toBe(false);
|
|
472
|
+
expect(output.summary).toBeDefined();
|
|
473
|
+
expect(output.results).toHaveLength(1);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
describe('MarkdownReporter', () => {
|
|
478
|
+
let reporter;
|
|
479
|
+
let consoleSpy;
|
|
480
|
+
|
|
481
|
+
beforeEach(() => {
|
|
482
|
+
reporter = new MarkdownReporter();
|
|
483
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
afterEach(() => {
|
|
487
|
+
consoleSpy.mockRestore();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should output markdown format', () => {
|
|
491
|
+
const result = {
|
|
492
|
+
passed: true,
|
|
493
|
+
summary: {
|
|
494
|
+
filesChecked: 5,
|
|
495
|
+
total: 0,
|
|
496
|
+
critical: 0,
|
|
497
|
+
error: 0,
|
|
498
|
+
warn: 0
|
|
499
|
+
},
|
|
500
|
+
results: []
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
reporter.report(result);
|
|
504
|
+
|
|
505
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
506
|
+
expect.stringContaining('# Quality Gate Report')
|
|
507
|
+
);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should show pass/fail status with emoji', () => {
|
|
511
|
+
const passResult = {
|
|
512
|
+
passed: true,
|
|
513
|
+
summary: { filesChecked: 1, total: 0, critical: 0, error: 0, warn: 0 },
|
|
514
|
+
results: []
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
reporter.report(passResult);
|
|
518
|
+
|
|
519
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
520
|
+
expect.stringContaining(':white_check_mark:')
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
jest.clearAllMocks();
|
|
524
|
+
|
|
525
|
+
const failResult = {
|
|
526
|
+
passed: false,
|
|
527
|
+
summary: { filesChecked: 1, total: 1, critical: 0, error: 1, warn: 0 },
|
|
528
|
+
results: [
|
|
529
|
+
{
|
|
530
|
+
file: '/test.js',
|
|
531
|
+
ruleName: 'Test Rule',
|
|
532
|
+
severity: 'error',
|
|
533
|
+
message: 'Test error',
|
|
534
|
+
pass: false
|
|
535
|
+
}
|
|
536
|
+
]
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
reporter.report(failResult);
|
|
540
|
+
|
|
541
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
542
|
+
expect.stringContaining(':x:')
|
|
543
|
+
);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('should include summary section', () => {
|
|
547
|
+
const result = {
|
|
548
|
+
passed: true,
|
|
549
|
+
summary: {
|
|
550
|
+
filesChecked: 10,
|
|
551
|
+
total: 5,
|
|
552
|
+
critical: 0,
|
|
553
|
+
error: 2,
|
|
554
|
+
warn: 3
|
|
555
|
+
},
|
|
556
|
+
results: []
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
reporter.report(result);
|
|
560
|
+
|
|
561
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
562
|
+
expect.stringContaining('## Summary')
|
|
563
|
+
);
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe('checkOrThrow', () => {
|
|
568
|
+
it('should return result when check passes', async () => {
|
|
569
|
+
const result = await checkOrThrow({
|
|
570
|
+
projectDir: tempDir,
|
|
571
|
+
severity: 'critical'
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
expect(result).toBeDefined();
|
|
575
|
+
expect(typeof result.passed).toBe('boolean');
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('should throw QualityGateError when check fails', async () => {
|
|
579
|
+
// Create a gate that will definitely fail
|
|
580
|
+
const failDir = path.join(os.tmpdir(), 'smc-fail-test-' + Date.now());
|
|
581
|
+
fs.mkdirSync(failDir, { recursive: true });
|
|
582
|
+
|
|
583
|
+
// Create a large file that will fail line-count-limit
|
|
584
|
+
const largeFile = path.join(failDir, 'large.js');
|
|
585
|
+
const lines = [];
|
|
586
|
+
for (let i = 0; i < 900; i++) {
|
|
587
|
+
lines.push(`const x = ${i};`);
|
|
588
|
+
}
|
|
589
|
+
fs.writeFileSync(largeFile, lines.join('\n'));
|
|
590
|
+
|
|
591
|
+
await expect(async () => {
|
|
592
|
+
await checkOrThrow({
|
|
593
|
+
projectDir: failDir,
|
|
594
|
+
severity: 'error'
|
|
595
|
+
});
|
|
596
|
+
}).rejects.toThrow(QualityGateError);
|
|
597
|
+
|
|
598
|
+
fs.rmSync(failDir, { recursive: true, force: true });
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('should include result in thrown error', async () => {
|
|
602
|
+
const failDir = path.join(os.tmpdir(), 'smc-fail-test2-' + Date.now());
|
|
603
|
+
fs.mkdirSync(failDir, { recursive: true });
|
|
604
|
+
|
|
605
|
+
const largeFile = path.join(failDir, 'large.js');
|
|
606
|
+
const lines = [];
|
|
607
|
+
for (let i = 0; i < 900; i++) {
|
|
608
|
+
lines.push(`const x = ${i};`);
|
|
609
|
+
}
|
|
610
|
+
fs.writeFileSync(largeFile, lines.join('\n'));
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
await checkOrThrow({
|
|
614
|
+
projectDir: failDir,
|
|
615
|
+
severity: 'error'
|
|
616
|
+
});
|
|
617
|
+
fail('Should have thrown');
|
|
618
|
+
} catch (e) {
|
|
619
|
+
expect(e instanceof QualityGateError).toBe(true);
|
|
620
|
+
expect(e.results).toBeDefined();
|
|
621
|
+
} finally {
|
|
622
|
+
fs.rmSync(failDir, { recursive: true, force: true });
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
describe('Integration Tests', () => {
|
|
628
|
+
it('should handle complete check workflow', async () => {
|
|
629
|
+
const gate = new QualityGate({
|
|
630
|
+
projectDir: tempDir,
|
|
631
|
+
reporters: []
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
const result = await gate.check();
|
|
635
|
+
|
|
636
|
+
expect(result).toMatchObject({
|
|
637
|
+
passed: expect.any(Boolean),
|
|
638
|
+
results: expect.any(Array),
|
|
639
|
+
summary: expect.objectContaining({
|
|
640
|
+
filesChecked: expect.any(Number),
|
|
641
|
+
rulesRun: expect.any(Number),
|
|
642
|
+
total: expect.any(Number)
|
|
643
|
+
})
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('should handle files with various issues', async () => {
|
|
648
|
+
const gate = new QualityGate({
|
|
649
|
+
projectDir: tempDir,
|
|
650
|
+
reporters: []
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const result = await gate.check();
|
|
654
|
+
|
|
655
|
+
// Should find issues in dirty file
|
|
656
|
+
const dirtyFileIssues = result.results.filter(r =>
|
|
657
|
+
r.file === dirtyJsFile || r.file.endsWith(path.basename(dirtyJsFile))
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
expect(dirtyFileIssues.length).toBeGreaterThan(0);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('should handle empty project directory', async () => {
|
|
664
|
+
const emptyDir = path.join(os.tmpdir(), 'smc-empty-' + Date.now());
|
|
665
|
+
fs.mkdirSync(emptyDir, { recursive: true });
|
|
666
|
+
|
|
667
|
+
const gate = new QualityGate({
|
|
668
|
+
projectDir: emptyDir,
|
|
669
|
+
reporters: []
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
const result = await gate.check();
|
|
673
|
+
|
|
674
|
+
expect(result.summary.filesChecked).toBe(0);
|
|
675
|
+
|
|
676
|
+
fs.rmSync(emptyDir, { recursive: true, force: true });
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
});
|