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.
Files changed (102) hide show
  1. package/.claude/hooks/code-formatter.cjs +7 -2
  2. package/.claude/hooks/multi-session.cjs +9 -3
  3. package/.claude/hooks/pre-commit.cjs +0 -0
  4. package/.claude/hooks/pre-push.cjs +0 -0
  5. package/.claude/hooks/project-kickoff.cjs +22 -11
  6. package/.claude/hooks/rag-skill-loader.cjs +7 -0
  7. package/.claude/hooks/thinking-silent.cjs +9 -3
  8. package/.claude/hooks/todo-manager.cjs +19 -13
  9. package/.claude/hooks/verify-work.cjs +10 -4
  10. package/.claude/quality-gate.json +9 -3
  11. package/.claude/settings.local.json +16 -1
  12. package/.claude/templates/hooks/README.md +302 -0
  13. package/.claude/templates/hooks/hook.sh.template +94 -0
  14. package/.claude/templates/hooks/user-prompt-submit.cjs.template +116 -0
  15. package/.claude/templates/hooks/user-response-submit.cjs.template +94 -0
  16. package/.claude/templates/hooks/validate.js +173 -0
  17. package/.claude/workflow/document-scanner.js +426 -0
  18. package/.claude/workflow/knowledge-engine.js +941 -0
  19. package/.claude/workflow/notebooklm/browser.js +1028 -0
  20. package/.claude/workflow/phases/phase1-research.js +578 -0
  21. package/.claude/workflow/phases/phase1-research.ts +465 -0
  22. package/.claude/workflow/phases/phase2-approve.js +722 -0
  23. package/.claude/workflow/phases/phase3-plan.js +1200 -0
  24. package/.claude/workflow/phases/phase4-develop.js +894 -0
  25. package/.claude/workflow/search-cache.js +230 -0
  26. package/.claude/workflow/templates/approval.md +315 -0
  27. package/.claude/workflow/templates/development.md +377 -0
  28. package/.claude/workflow/templates/planning.md +328 -0
  29. package/.claude/workflow/templates/research.md +250 -0
  30. package/.claude/workflow/types.js +37 -0
  31. package/.claude/workflow/web-search.js +278 -0
  32. package/.claude-plugin/marketplace.json +2 -2
  33. package/AGENTS.md +176 -0
  34. package/CHANGELOG.md +7 -14
  35. package/cli.js +20 -0
  36. package/config/quality-gate.json +9 -3
  37. package/development/cache/web-search/search_1193d605f8eb364651fc2f2041b58a31.json +36 -0
  38. package/development/cache/web-search/search_3798bf06960edc125f744a1abb5b72c5.json +36 -0
  39. package/development/cache/web-search/search_37c7d4843a53f0d83f1122a6f908a2a3.json +36 -0
  40. package/development/cache/web-search/search_44166fa0153709ee168485a22aa0ab40.json +36 -0
  41. package/development/cache/web-search/search_4deaebb1f77e86a8ca066dc5a49c59fd.json +36 -0
  42. package/development/cache/web-search/search_94da91789466070a7f545612e73c7372.json +36 -0
  43. package/development/cache/web-search/search_dd5de8491b8b803a3cb01339cd210fb0.json +36 -0
  44. package/development/knowledge-base/.index.clean.json +0 -0
  45. package/development/knowledge-base/.index.json +486 -0
  46. package/development/knowledge-base/test-best-practices.md +29 -0
  47. package/development/projects/proj_mkh1pazz_ixmt1/phase1/feasibility-report.md +160 -0
  48. package/development/projects/proj_mkh4jvnb_z7rwf/phase1/feasibility-report.md +160 -0
  49. package/development/projects/proj_mkh4jxkd_ewz5a/phase1/feasibility-report.md +160 -0
  50. package/development/projects/proj_mkh4k84n_ni73k/phase1/feasibility-report.md +160 -0
  51. package/development/projects/proj_mkh4wfyd_u9w88/phase1/feasibility-report.md +160 -0
  52. package/development/projects/proj_mkh4wsbo_iahvf/development/projects/proj_mkh4xbpg_4na5w/phase1/feasibility-report.md +160 -0
  53. package/development/projects/proj_mkh4wsbo_iahvf/phase1/feasibility-report.md +160 -0
  54. package/development/projects/proj_mkh4xulg_1ka8x/phase1/feasibility-report.md +160 -0
  55. package/development/projects/proj_mkh4xwhj_gch8j/phase1/feasibility-report.md +160 -0
  56. package/development/projects/proj_mkh4y2qk_9lm8z/phase1/feasibility-report.md +160 -0
  57. package/development/projects/proj_mkh4y2qk_9lm8z/phase2/requirements.md +226 -0
  58. package/development/projects/proj_mkh4y2qk_9lm8z/phase3/PRD.md +345 -0
  59. package/development/projects/proj_mkh4y2qk_9lm8z/phase3/TASK_PLAN.md +284 -0
  60. package/development/projects/proj_mkh4y2qk_9lm8z/phase3/prototype/README.md +14 -0
  61. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/DEVELOPMENT_LOG.md +35 -0
  62. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/TASKS.md +34 -0
  63. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/.env.example +5 -0
  64. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/README.md +60 -0
  65. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/package.json +25 -0
  66. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/src/index.js +70 -0
  67. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/src/routes/index.js +48 -0
  68. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/tests/health.test.js +20 -0
  69. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/tests/jest.config.js +21 -0
  70. package/development/projects/proj_mkh7veqg_3lypc/phase1/feasibility-report.md +160 -0
  71. package/development/projects/proj_mkh7veqg_3lypc/phase2/requirements.md +226 -0
  72. package/development/projects/proj_mkh7veqg_3lypc/phase3/PRD.md +345 -0
  73. package/development/projects/proj_mkh7veqg_3lypc/phase3/TASK_PLAN.md +284 -0
  74. package/development/projects/proj_mkh7veqg_3lypc/phase3/prototype/README.md +14 -0
  75. package/development/projects/proj_mkh8k8fo_rmqn5/phase1/feasibility-report.md +160 -0
  76. package/development/projects/proj_mkh8xyhy_1vshq/phase1/feasibility-report.md +178 -0
  77. package/development/projects/proj_mkh8zddd_dhamf/phase1/feasibility-report.md +377 -0
  78. package/development/projects/proj_mkh8zddd_dhamf/phase2/requirements.md +442 -0
  79. package/development/projects/proj_mkh8zddd_dhamf/phase3/api-design.md +800 -0
  80. package/development/projects/proj_mkh8zddd_dhamf/phase3/architecture.md +625 -0
  81. package/development/projects/proj_mkh8zddd_dhamf/phase3/data-model.md +830 -0
  82. package/development/projects/proj_mkh8zddd_dhamf/phase3/risks.md +957 -0
  83. package/development/projects/proj_mkh8zddd_dhamf/phase3/wbs.md +381 -0
  84. package/development/todos/.state.json +14 -1
  85. package/development/todos/INDEX.md +31 -73
  86. package/development/todos/completed/develop/local-knowledge-index.md +85 -0
  87. package/development/todos/{active → completed/develop}/todo-system.md +13 -3
  88. package/development/todos/completed/develop/web-search-integration.md +83 -0
  89. package/development/todos/completed/test/phase1-e2e-test.md +103 -0
  90. package/lib/commands.js +388 -0
  91. package/package.json +3 -2
  92. package/tests/config-manager.test.js +677 -0
  93. package/tests/config-validator.test.js +436 -0
  94. package/tests/errors.test.js +477 -0
  95. package/tests/manual/phase1-e2e.sh +389 -0
  96. package/tests/manual/phase2-test-cases.md +311 -0
  97. package/tests/manual/phase3-test-cases.md +309 -0
  98. package/tests/manual/phase4-test-cases.md +414 -0
  99. package/tests/manual/test-cases.md +417 -0
  100. package/tests/quality-gate.test.js +679 -0
  101. package/tests/quality-rules.test.js +619 -0
  102. 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
+ });