tlc-claude-code 1.0.0 → 1.1.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/README.md CHANGED
@@ -290,10 +290,91 @@ Commands install to `.claude/commands/tlc/`
290
290
 
291
291
  ---
292
292
 
293
+ ## VPS Deployment
294
+
295
+ Deploy TLC server for your team on any VPS.
296
+
297
+ ### Quick Setup (Ubuntu)
298
+
299
+ ```bash
300
+ curl -fsSL https://raw.githubusercontent.com/jurgencalleja/TLC/main/scripts/vps-setup.sh | bash
301
+ ```
302
+
303
+ ### What You Get
304
+
305
+ | URL | Service |
306
+ |-----|---------|
307
+ | `https://dashboard.project.com` | TLC Dashboard with auth |
308
+ | `https://main.project.com` | Main branch deployment |
309
+ | `https://feat-x.project.com` | Feature branch deployment |
310
+
311
+ ### Requirements
312
+
313
+ - Ubuntu 22.04+ VPS (2GB+ RAM)
314
+ - Domain with wildcard DNS (`*.project.com → VPS_IP`)
315
+ - GitHub/GitLab repo access
316
+
317
+ ### Manual Setup
318
+
319
+ 1. **Install dependencies**
320
+ ```bash
321
+ apt install docker.io nginx certbot nodejs npm postgresql
322
+ ```
323
+
324
+ 2. **Clone and configure**
325
+ ```bash
326
+ git clone https://github.com/jurgencalleja/TLC.git /opt/tlc
327
+ cd /opt/tlc && npm install
328
+ cp .env.example .env # Edit with your settings
329
+ ```
330
+
331
+ 3. **Setup nginx + SSL**
332
+ ```bash
333
+ certbot --nginx -d "*.project.com" -d "dashboard.project.com"
334
+ ```
335
+
336
+ 4. **Start server**
337
+ ```bash
338
+ systemctl enable tlc && systemctl start tlc
339
+ ```
340
+
341
+ 5. **Configure webhook** in GitHub/GitLab repo settings
342
+
343
+ [**Full VPS Guide →**](docs/vps-deployment.md)
344
+
345
+ ---
346
+
347
+ ## Kubernetes Deployment
348
+
349
+ For teams using Kubernetes:
350
+
351
+ ```bash
352
+ # Add Helm repo
353
+ helm repo add tlc https://jurgencalleja.github.io/TLC/charts
354
+
355
+ # Install
356
+ helm install tlc tlc/tlc-server \
357
+ --set domain=project.example.com \
358
+ --set slack.webhookUrl=https://hooks.slack.com/...
359
+ ```
360
+
361
+ ### Kubernetes Features
362
+
363
+ - **Auto-scaling** branch deployments per namespace
364
+ - **Ingress** with wildcard TLS
365
+ - **Persistent volumes** for deployment state
366
+ - **ConfigMaps** for environment config
367
+
368
+ [**Full K8s Guide →**](docs/kubernetes-deployment.md)
369
+
370
+ ---
371
+
293
372
  ## Documentation
294
373
 
295
374
  - **[Help / All Commands](help.md)** — Complete command reference
296
375
  - **[Team Workflow](docs/team-workflow.md)** — Guide for teams (engineers + PO + QA)
376
+ - **[VPS Deployment](docs/vps-deployment.md)** — Deploy on Ubuntu VPS
377
+ - **[Kubernetes Deployment](docs/kubernetes-deployment.md)** — Deploy on K8s
297
378
 
298
379
  ---
299
380
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tlc-claude-code",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "TLC - Test Led Coding for Claude Code",
5
5
  "bin": {
6
6
  "tlc": "./bin/tlc.js",
@@ -31,7 +31,7 @@ function parseOverdriveArgs(args = '') {
31
31
  if (/^\d+$/.test(part)) {
32
32
  options.phase = parseInt(part, 10);
33
33
  } else if (part === '--agents' && parts[i + 1]) {
34
- options.agents = Math.min(parseInt(parts[++i], 10), 5); // Max 5 agents
34
+ options.agents = Math.min(parseInt(parts[++i], 10), 10); // Max 10 agents
35
35
  } else if (part === '--mode' && parts[i + 1]) {
36
36
  options.mode = parts[++i];
37
37
  } else if (part === '--dry-run') {
@@ -33,9 +33,9 @@ describe('overdrive-command', () => {
33
33
  expect(options.agents).toBe(4);
34
34
  });
35
35
 
36
- it('caps agents at 5', () => {
37
- const options = parseOverdriveArgs('--agents 10');
38
- expect(options.agents).toBe(5);
36
+ it('caps agents at 10', () => {
37
+ const options = parseOverdriveArgs('--agents 15');
38
+ expect(options.agents).toBe(10);
39
39
  });
40
40
 
41
41
  it('parses --mode flag', () => {
@@ -0,0 +1,463 @@
1
+ /**
2
+ * PR Reviewer Module
3
+ * Automatically reviews code changes for TLC compliance
4
+ */
5
+
6
+ const { execSync } = require('child_process');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ /**
11
+ * Get changed files between two refs
12
+ * @param {string} base - Base ref (e.g., 'main')
13
+ * @param {string} head - Head ref (e.g., 'HEAD')
14
+ * @param {string} cwd - Working directory
15
+ * @returns {Array} List of changed files with status
16
+ */
17
+ function getChangedFiles(base = 'main', head = 'HEAD', cwd = process.cwd()) {
18
+ try {
19
+ const output = execSync(`git diff --name-status ${base}...${head}`, {
20
+ cwd,
21
+ encoding: 'utf-8',
22
+ });
23
+
24
+ return output
25
+ .trim()
26
+ .split('\n')
27
+ .filter(Boolean)
28
+ .map(line => {
29
+ const [status, ...fileParts] = line.split('\t');
30
+ return {
31
+ status: status.trim(),
32
+ file: fileParts.join('\t').trim(),
33
+ isTest: isTestFile(fileParts.join('\t').trim()),
34
+ };
35
+ });
36
+ } catch (e) {
37
+ return [];
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Check if a file is a test file
43
+ * @param {string} filePath - File path
44
+ * @returns {boolean}
45
+ */
46
+ function isTestFile(filePath) {
47
+ const testPatterns = [
48
+ /\.test\.[jt]sx?$/,
49
+ /\.spec\.[jt]sx?$/,
50
+ /test_.*\.py$/,
51
+ /_test\.py$/,
52
+ /_test\.go$/,
53
+ /\.test\.go$/,
54
+ /spec\/.*_spec\.rb$/,
55
+ /__tests__\//,
56
+ /tests?\//i,
57
+ ];
58
+ return testPatterns.some(p => p.test(filePath));
59
+ }
60
+
61
+ /**
62
+ * Check if implementation file has corresponding test
63
+ * @param {string} implFile - Implementation file path
64
+ * @param {Array} allFiles - All changed files
65
+ * @param {string} cwd - Working directory
66
+ * @returns {Object} Test coverage info
67
+ */
68
+ function findTestForFile(implFile, allFiles, cwd = process.cwd()) {
69
+ // Skip if it's already a test file
70
+ if (isTestFile(implFile)) {
71
+ return { hasTest: true, isTestFile: true };
72
+ }
73
+
74
+ // Skip non-code files
75
+ const codeExtensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rb'];
76
+ const ext = path.extname(implFile);
77
+ if (!codeExtensions.includes(ext)) {
78
+ return { hasTest: true, skipped: true, reason: 'non-code file' };
79
+ }
80
+
81
+ // Generate possible test file names
82
+ const baseName = path.basename(implFile, ext);
83
+ const dirName = path.dirname(implFile);
84
+
85
+ const possibleTestFiles = [
86
+ // Same directory patterns
87
+ `${dirName}/${baseName}.test${ext}`,
88
+ `${dirName}/${baseName}.spec${ext}`,
89
+ `${dirName}/__tests__/${baseName}.test${ext}`,
90
+ `${dirName}/__tests__/${baseName}${ext}`,
91
+ // Test directory patterns
92
+ `test/${dirName}/${baseName}.test${ext}`,
93
+ `tests/${dirName}/${baseName}.test${ext}`,
94
+ `test/${baseName}.test${ext}`,
95
+ `tests/${baseName}.test${ext}`,
96
+ // Python patterns
97
+ `${dirName}/test_${baseName}.py`,
98
+ `tests/test_${baseName}.py`,
99
+ // Go patterns
100
+ `${dirName}/${baseName}_test.go`,
101
+ ];
102
+
103
+ // Check if any test file exists in changed files
104
+ const changedTestFile = allFiles.find(
105
+ f => f.isTest && possibleTestFiles.some(p => f.file.includes(baseName))
106
+ );
107
+
108
+ if (changedTestFile) {
109
+ return { hasTest: true, testFile: changedTestFile.file, inChangeset: true };
110
+ }
111
+
112
+ // Check if test file exists on disk
113
+ for (const testFile of possibleTestFiles) {
114
+ const fullPath = path.join(cwd, testFile);
115
+ if (fs.existsSync(fullPath)) {
116
+ return { hasTest: true, testFile, existsOnDisk: true };
117
+ }
118
+ }
119
+
120
+ return { hasTest: false, searchedPatterns: possibleTestFiles.slice(0, 3) };
121
+ }
122
+
123
+ /**
124
+ * Analyze commit order to verify test-first development
125
+ * @param {string} base - Base ref
126
+ * @param {string} head - Head ref
127
+ * @param {string} cwd - Working directory
128
+ * @returns {Object} Commit order analysis
129
+ */
130
+ function analyzeCommitOrder(base = 'main', head = 'HEAD', cwd = process.cwd()) {
131
+ try {
132
+ const output = execSync(
133
+ `git log --oneline --name-status ${base}..${head}`,
134
+ { cwd, encoding: 'utf-8' }
135
+ );
136
+
137
+ const commits = [];
138
+ let currentCommit = null;
139
+
140
+ for (const line of output.split('\n')) {
141
+ if (/^[a-f0-9]{7,}/.test(line)) {
142
+ if (currentCommit) commits.push(currentCommit);
143
+ const [hash, ...msgParts] = line.split(' ');
144
+ currentCommit = {
145
+ hash,
146
+ message: msgParts.join(' '),
147
+ files: [],
148
+ hasTests: false,
149
+ hasImpl: false,
150
+ };
151
+ } else if (currentCommit && line.trim()) {
152
+ const [status, file] = line.split('\t');
153
+ if (file) {
154
+ const isTest = isTestFile(file);
155
+ currentCommit.files.push({ status, file, isTest });
156
+ if (isTest) currentCommit.hasTests = true;
157
+ else currentCommit.hasImpl = true;
158
+ }
159
+ }
160
+ }
161
+ if (currentCommit) commits.push(currentCommit);
162
+
163
+ // Analyze TDD compliance
164
+ const analysis = {
165
+ commits: commits.length,
166
+ testFirstCommits: 0,
167
+ implOnlyCommits: 0,
168
+ mixedCommits: 0,
169
+ violations: [],
170
+ };
171
+
172
+ for (const commit of commits) {
173
+ if (commit.hasTests && !commit.hasImpl) {
174
+ analysis.testFirstCommits++;
175
+ } else if (commit.hasImpl && !commit.hasTests) {
176
+ analysis.implOnlyCommits++;
177
+ // Check if it's a fix/refactor (acceptable)
178
+ const isFixOrRefactor = /^(fix|refactor|chore|docs|style):/i.test(commit.message);
179
+ if (!isFixOrRefactor) {
180
+ analysis.violations.push({
181
+ commit: commit.hash,
182
+ message: commit.message,
183
+ reason: 'Implementation without tests',
184
+ });
185
+ }
186
+ } else if (commit.hasTests && commit.hasImpl) {
187
+ analysis.mixedCommits++;
188
+ }
189
+ }
190
+
191
+ analysis.tddScore = commits.length > 0
192
+ ? Math.round((analysis.testFirstCommits / commits.length) * 100)
193
+ : 100;
194
+
195
+ return analysis;
196
+ } catch (e) {
197
+ return { error: e.message };
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Check for common security issues in diff
203
+ * @param {string} base - Base ref
204
+ * @param {string} head - Head ref
205
+ * @param {string} cwd - Working directory
206
+ * @returns {Array} Security issues found
207
+ */
208
+ function checkSecurityIssues(base = 'main', head = 'HEAD', cwd = process.cwd()) {
209
+ const issues = [];
210
+
211
+ try {
212
+ const diff = execSync(`git diff ${base}...${head}`, {
213
+ cwd,
214
+ encoding: 'utf-8',
215
+ maxBuffer: 10 * 1024 * 1024,
216
+ });
217
+
218
+ const patterns = [
219
+ { pattern: /password\s*=\s*['"][^'"]+['"]/gi, type: 'hardcoded-password', severity: 'high' },
220
+ { pattern: /api[_-]?key\s*=\s*['"][^'"]+['"]/gi, type: 'hardcoded-api-key', severity: 'high' },
221
+ { pattern: /secret\s*=\s*['"][^'"]+['"]/gi, type: 'hardcoded-secret', severity: 'high' },
222
+ { pattern: /eval\s*\(/g, type: 'eval-usage', severity: 'medium' },
223
+ { pattern: /innerHTML\s*=/g, type: 'innerhtml-xss', severity: 'medium' },
224
+ { pattern: /dangerouslySetInnerHTML/g, type: 'react-xss', severity: 'medium' },
225
+ { pattern: /exec\s*\(\s*[`'"]/g, type: 'command-injection', severity: 'high' },
226
+ { pattern: /SELECT.*FROM.*WHERE.*\+/gi, type: 'sql-injection', severity: 'high' },
227
+ { pattern: /\.env(?:\.local)?$/gm, type: 'env-file-committed', severity: 'high' },
228
+ { pattern: /console\.log\(/g, type: 'console-log', severity: 'low' },
229
+ { pattern: /TODO|FIXME|HACK|XXX/g, type: 'todo-comment', severity: 'info' },
230
+ ];
231
+
232
+ // Only check added lines
233
+ const addedLines = diff
234
+ .split('\n')
235
+ .filter(line => line.startsWith('+') && !line.startsWith('+++'));
236
+
237
+ for (const { pattern, type, severity } of patterns) {
238
+ for (const line of addedLines) {
239
+ if (pattern.test(line)) {
240
+ issues.push({
241
+ type,
242
+ severity,
243
+ line: line.slice(1).trim().slice(0, 100),
244
+ });
245
+ }
246
+ }
247
+ }
248
+ } catch (e) {
249
+ // Ignore diff errors
250
+ }
251
+
252
+ return issues;
253
+ }
254
+
255
+ /**
256
+ * Generate review report
257
+ * @param {Object} options - Review options
258
+ * @returns {Object} Review report
259
+ */
260
+ function generateReview(options = {}) {
261
+ const {
262
+ base = 'main',
263
+ head = 'HEAD',
264
+ cwd = process.cwd(),
265
+ prNumber = null,
266
+ } = options;
267
+
268
+ const report = {
269
+ timestamp: new Date().toISOString(),
270
+ base,
271
+ head,
272
+ prNumber,
273
+ passed: true,
274
+ summary: [],
275
+ details: {},
276
+ };
277
+
278
+ // 1. Get changed files
279
+ const changedFiles = getChangedFiles(base, head, cwd);
280
+ report.details.changedFiles = changedFiles;
281
+ report.details.fileCount = changedFiles.length;
282
+
283
+ // 2. Check test coverage for changed files
284
+ const coverageIssues = [];
285
+ const implFiles = changedFiles.filter(f => !f.isTest && f.status !== 'D');
286
+
287
+ for (const file of implFiles) {
288
+ const testInfo = findTestForFile(file.file, changedFiles, cwd);
289
+ if (!testInfo.hasTest) {
290
+ coverageIssues.push({
291
+ file: file.file,
292
+ issue: 'No test file found',
293
+ suggestions: testInfo.searchedPatterns,
294
+ });
295
+ }
296
+ }
297
+
298
+ report.details.coverage = {
299
+ implFiles: implFiles.length,
300
+ testFiles: changedFiles.filter(f => f.isTest).length,
301
+ missingTests: coverageIssues.length,
302
+ issues: coverageIssues,
303
+ };
304
+
305
+ if (coverageIssues.length > 0) {
306
+ report.passed = false;
307
+ report.summary.push(`❌ ${coverageIssues.length} files missing tests`);
308
+ } else {
309
+ report.summary.push(`✅ All changed files have tests`);
310
+ }
311
+
312
+ // 3. Analyze commit order (TDD compliance)
313
+ const commitAnalysis = analyzeCommitOrder(base, head, cwd);
314
+ report.details.commits = commitAnalysis;
315
+
316
+ if (commitAnalysis.tddScore !== undefined) {
317
+ if (commitAnalysis.tddScore < 50 && commitAnalysis.commits > 2) {
318
+ report.passed = false;
319
+ report.summary.push(`❌ TDD score: ${commitAnalysis.tddScore}% (target: 50%+)`);
320
+ } else {
321
+ report.summary.push(`✅ TDD score: ${commitAnalysis.tddScore}%`);
322
+ }
323
+
324
+ if (commitAnalysis.violations.length > 0) {
325
+ report.summary.push(`⚠️ ${commitAnalysis.violations.length} commits without tests`);
326
+ }
327
+ }
328
+
329
+ // 4. Security check
330
+ const securityIssues = checkSecurityIssues(base, head, cwd);
331
+ report.details.security = securityIssues;
332
+
333
+ const highSeverity = securityIssues.filter(i => i.severity === 'high');
334
+ const mediumSeverity = securityIssues.filter(i => i.severity === 'medium');
335
+
336
+ if (highSeverity.length > 0) {
337
+ report.passed = false;
338
+ report.summary.push(`❌ ${highSeverity.length} high severity security issues`);
339
+ }
340
+ if (mediumSeverity.length > 0) {
341
+ report.summary.push(`⚠️ ${mediumSeverity.length} medium severity security issues`);
342
+ }
343
+ if (highSeverity.length === 0 && mediumSeverity.length === 0) {
344
+ report.summary.push(`✅ No security issues detected`);
345
+ }
346
+
347
+ // 5. Overall verdict
348
+ report.verdict = report.passed ? 'APPROVED' : 'CHANGES_REQUESTED';
349
+
350
+ return report;
351
+ }
352
+
353
+ /**
354
+ * Format review report as markdown
355
+ * @param {Object} report - Review report
356
+ * @returns {string} Markdown formatted report
357
+ */
358
+ function formatReviewMarkdown(report) {
359
+ const lines = [];
360
+
361
+ lines.push('# Code Review Report');
362
+ lines.push('');
363
+ lines.push(`**Date:** ${report.timestamp}`);
364
+ lines.push(`**Base:** ${report.base} → **Head:** ${report.head}`);
365
+ if (report.prNumber) {
366
+ lines.push(`**PR:** #${report.prNumber}`);
367
+ }
368
+ lines.push('');
369
+
370
+ // Verdict
371
+ const verdictEmoji = report.passed ? '✅' : '❌';
372
+ lines.push(`## ${verdictEmoji} Verdict: ${report.verdict}`);
373
+ lines.push('');
374
+
375
+ // Summary
376
+ lines.push('## Summary');
377
+ lines.push('');
378
+ for (const item of report.summary) {
379
+ lines.push(`- ${item}`);
380
+ }
381
+ lines.push('');
382
+
383
+ // Coverage details
384
+ if (report.details.coverage.missingTests > 0) {
385
+ lines.push('## Missing Tests');
386
+ lines.push('');
387
+ lines.push('| File | Suggested Test Location |');
388
+ lines.push('|------|------------------------|');
389
+ for (const issue of report.details.coverage.issues) {
390
+ const suggestion = issue.suggestions?.[0] || 'N/A';
391
+ lines.push(`| \`${issue.file}\` | \`${suggestion}\` |`);
392
+ }
393
+ lines.push('');
394
+ }
395
+
396
+ // TDD violations
397
+ if (report.details.commits.violations?.length > 0) {
398
+ lines.push('## TDD Violations');
399
+ lines.push('');
400
+ lines.push('| Commit | Message | Issue |');
401
+ lines.push('|--------|---------|-------|');
402
+ for (const v of report.details.commits.violations) {
403
+ lines.push(`| \`${v.commit}\` | ${v.message.slice(0, 40)} | ${v.reason} |`);
404
+ }
405
+ lines.push('');
406
+ }
407
+
408
+ // Security issues
409
+ const importantSecurity = report.details.security.filter(
410
+ i => i.severity === 'high' || i.severity === 'medium'
411
+ );
412
+ if (importantSecurity.length > 0) {
413
+ lines.push('## Security Issues');
414
+ lines.push('');
415
+ lines.push('| Severity | Type | Sample |');
416
+ lines.push('|----------|------|--------|');
417
+ for (const issue of importantSecurity) {
418
+ lines.push(`| ${issue.severity.toUpperCase()} | ${issue.type} | \`${issue.line.slice(0, 50)}\` |`);
419
+ }
420
+ lines.push('');
421
+ }
422
+
423
+ // Stats
424
+ lines.push('## Statistics');
425
+ lines.push('');
426
+ lines.push(`- Files changed: ${report.details.fileCount}`);
427
+ lines.push(`- Implementation files: ${report.details.coverage.implFiles}`);
428
+ lines.push(`- Test files: ${report.details.coverage.testFiles}`);
429
+ if (report.details.commits.commits) {
430
+ lines.push(`- Commits: ${report.details.commits.commits}`);
431
+ lines.push(`- TDD Score: ${report.details.commits.tddScore}%`);
432
+ }
433
+
434
+ return lines.join('\n');
435
+ }
436
+
437
+ /**
438
+ * Run review and return result
439
+ * @param {Object} options - Review options
440
+ * @returns {Object} Review result with report and formatted output
441
+ */
442
+ function runReview(options = {}) {
443
+ const report = generateReview(options);
444
+ const markdown = formatReviewMarkdown(report);
445
+
446
+ return {
447
+ report,
448
+ markdown,
449
+ passed: report.passed,
450
+ verdict: report.verdict,
451
+ };
452
+ }
453
+
454
+ module.exports = {
455
+ getChangedFiles,
456
+ isTestFile,
457
+ findTestForFile,
458
+ analyzeCommitOrder,
459
+ checkSecurityIssues,
460
+ generateReview,
461
+ formatReviewMarkdown,
462
+ runReview,
463
+ };
@@ -0,0 +1,268 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ isTestFile,
4
+ formatReviewMarkdown,
5
+ } from './pr-reviewer.js';
6
+
7
+ describe('pr-reviewer', () => {
8
+ describe('isTestFile', () => {
9
+ it('identifies JavaScript test files', () => {
10
+ expect(isTestFile('src/auth.test.js')).toBe(true);
11
+ expect(isTestFile('src/auth.spec.js')).toBe(true);
12
+ expect(isTestFile('src/__tests__/auth.js')).toBe(true);
13
+ });
14
+
15
+ it('identifies TypeScript test files', () => {
16
+ expect(isTestFile('src/auth.test.ts')).toBe(true);
17
+ expect(isTestFile('src/auth.spec.tsx')).toBe(true);
18
+ });
19
+
20
+ it('identifies Python test files', () => {
21
+ expect(isTestFile('test_auth.py')).toBe(true);
22
+ expect(isTestFile('auth_test.py')).toBe(true);
23
+ expect(isTestFile('tests/test_login.py')).toBe(true);
24
+ });
25
+
26
+ it('identifies Go test files', () => {
27
+ expect(isTestFile('auth_test.go')).toBe(true);
28
+ expect(isTestFile('pkg/auth.test.go')).toBe(true);
29
+ });
30
+
31
+ it('identifies Ruby spec files', () => {
32
+ expect(isTestFile('spec/auth_spec.rb')).toBe(true);
33
+ });
34
+
35
+ it('identifies files in test directories', () => {
36
+ expect(isTestFile('test/helpers.js')).toBe(true);
37
+ expect(isTestFile('tests/utils.js')).toBe(true);
38
+ });
39
+
40
+ it('returns false for implementation files', () => {
41
+ expect(isTestFile('src/auth.js')).toBe(false);
42
+ expect(isTestFile('lib/utils.ts')).toBe(false);
43
+ expect(isTestFile('main.py')).toBe(false);
44
+ });
45
+
46
+ it('returns false for config files', () => {
47
+ expect(isTestFile('package.json')).toBe(false);
48
+ expect(isTestFile('.eslintrc.js')).toBe(false);
49
+ expect(isTestFile('tsconfig.json')).toBe(false);
50
+ });
51
+
52
+ it('returns false for documentation', () => {
53
+ expect(isTestFile('README.md')).toBe(false);
54
+ expect(isTestFile('docs/api.md')).toBe(false);
55
+ });
56
+ });
57
+
58
+ describe('formatReviewMarkdown', () => {
59
+ it('formats passing review', () => {
60
+ const report = {
61
+ timestamp: '2024-01-01T00:00:00Z',
62
+ base: 'main',
63
+ head: 'feature',
64
+ passed: true,
65
+ verdict: 'APPROVED',
66
+ summary: ['✅ All tests pass'],
67
+ details: {
68
+ fileCount: 5,
69
+ coverage: { implFiles: 3, testFiles: 2, missingTests: 0, issues: [] },
70
+ commits: { commits: 2, tddScore: 100, violations: [] },
71
+ security: [],
72
+ },
73
+ };
74
+
75
+ const markdown = formatReviewMarkdown(report);
76
+
77
+ expect(markdown).toContain('# Code Review Report');
78
+ expect(markdown).toContain('APPROVED');
79
+ expect(markdown).toContain('✅ All tests pass');
80
+ expect(markdown).toContain('main');
81
+ expect(markdown).toContain('feature');
82
+ });
83
+
84
+ it('formats failing review with missing tests', () => {
85
+ const report = {
86
+ timestamp: '2024-01-01T00:00:00Z',
87
+ base: 'main',
88
+ head: 'feature',
89
+ passed: false,
90
+ verdict: 'CHANGES_REQUESTED',
91
+ summary: ['❌ 2 files missing tests'],
92
+ details: {
93
+ fileCount: 5,
94
+ coverage: {
95
+ implFiles: 3,
96
+ testFiles: 1,
97
+ missingTests: 2,
98
+ issues: [
99
+ { file: 'src/a.js', suggestions: ['src/a.test.js'] },
100
+ { file: 'src/b.js', suggestions: ['src/b.test.js'] },
101
+ ],
102
+ },
103
+ commits: { commits: 1, tddScore: 50, violations: [] },
104
+ security: [],
105
+ },
106
+ };
107
+
108
+ const markdown = formatReviewMarkdown(report);
109
+
110
+ expect(markdown).toContain('CHANGES_REQUESTED');
111
+ expect(markdown).toContain('Missing Tests');
112
+ expect(markdown).toContain('src/a.js');
113
+ expect(markdown).toContain('src/b.js');
114
+ });
115
+
116
+ it('formats failing review with TDD violations', () => {
117
+ const report = {
118
+ timestamp: '2024-01-01T00:00:00Z',
119
+ base: 'main',
120
+ head: 'feature',
121
+ passed: false,
122
+ verdict: 'CHANGES_REQUESTED',
123
+ summary: ['❌ TDD violations'],
124
+ details: {
125
+ fileCount: 3,
126
+ coverage: { implFiles: 2, testFiles: 1, missingTests: 0, issues: [] },
127
+ commits: {
128
+ commits: 2,
129
+ tddScore: 0,
130
+ violations: [
131
+ { commit: 'abc1234', message: 'feat: add feature', reason: 'No tests' },
132
+ ],
133
+ },
134
+ security: [],
135
+ },
136
+ };
137
+
138
+ const markdown = formatReviewMarkdown(report);
139
+
140
+ expect(markdown).toContain('TDD Violations');
141
+ expect(markdown).toContain('abc1234');
142
+ expect(markdown).toContain('No tests');
143
+ });
144
+
145
+ it('formats failing review with security issues', () => {
146
+ const report = {
147
+ timestamp: '2024-01-01T00:00:00Z',
148
+ base: 'main',
149
+ head: 'feature',
150
+ passed: false,
151
+ verdict: 'CHANGES_REQUESTED',
152
+ summary: ['❌ Security issues'],
153
+ details: {
154
+ fileCount: 2,
155
+ coverage: { implFiles: 1, testFiles: 1, missingTests: 0, issues: [] },
156
+ commits: { commits: 1, tddScore: 100, violations: [] },
157
+ security: [
158
+ { type: 'hardcoded-password', severity: 'high', line: 'password = "secret"' },
159
+ { type: 'eval-usage', severity: 'medium', line: 'eval(input)' },
160
+ ],
161
+ },
162
+ };
163
+
164
+ const markdown = formatReviewMarkdown(report);
165
+
166
+ expect(markdown).toContain('Security Issues');
167
+ expect(markdown).toContain('HIGH');
168
+ expect(markdown).toContain('hardcoded-password');
169
+ expect(markdown).toContain('MEDIUM');
170
+ expect(markdown).toContain('eval-usage');
171
+ });
172
+
173
+ it('includes PR number when present', () => {
174
+ const report = {
175
+ timestamp: '2024-01-01T00:00:00Z',
176
+ base: 'main',
177
+ head: 'feature',
178
+ prNumber: 42,
179
+ passed: true,
180
+ verdict: 'APPROVED',
181
+ summary: [],
182
+ details: {
183
+ fileCount: 0,
184
+ coverage: { implFiles: 0, testFiles: 0, missingTests: 0, issues: [] },
185
+ commits: {},
186
+ security: [],
187
+ },
188
+ };
189
+
190
+ const markdown = formatReviewMarkdown(report);
191
+
192
+ expect(markdown).toContain('**PR:** #42');
193
+ });
194
+
195
+ it('includes statistics', () => {
196
+ const report = {
197
+ timestamp: '2024-01-01T00:00:00Z',
198
+ base: 'main',
199
+ head: 'feature',
200
+ passed: true,
201
+ verdict: 'APPROVED',
202
+ summary: [],
203
+ details: {
204
+ fileCount: 10,
205
+ coverage: { implFiles: 6, testFiles: 4, missingTests: 0, issues: [] },
206
+ commits: { commits: 5, tddScore: 80, violations: [] },
207
+ security: [],
208
+ },
209
+ };
210
+
211
+ const markdown = formatReviewMarkdown(report);
212
+
213
+ expect(markdown).toContain('Statistics');
214
+ expect(markdown).toContain('Files changed: 10');
215
+ expect(markdown).toContain('Implementation files: 6');
216
+ expect(markdown).toContain('Test files: 4');
217
+ expect(markdown).toContain('Commits: 5');
218
+ expect(markdown).toContain('TDD Score: 80%');
219
+ });
220
+
221
+ it('handles empty report gracefully', () => {
222
+ const report = {
223
+ timestamp: '2024-01-01T00:00:00Z',
224
+ base: 'main',
225
+ head: 'HEAD',
226
+ passed: true,
227
+ verdict: 'APPROVED',
228
+ summary: [],
229
+ details: {
230
+ fileCount: 0,
231
+ coverage: { implFiles: 0, testFiles: 0, missingTests: 0, issues: [] },
232
+ commits: {},
233
+ security: [],
234
+ },
235
+ };
236
+
237
+ const markdown = formatReviewMarkdown(report);
238
+
239
+ expect(markdown).toContain('# Code Review Report');
240
+ expect(markdown).toContain('APPROVED');
241
+ });
242
+
243
+ it('skips low severity security issues in table', () => {
244
+ const report = {
245
+ timestamp: '2024-01-01T00:00:00Z',
246
+ base: 'main',
247
+ head: 'feature',
248
+ passed: true,
249
+ verdict: 'APPROVED',
250
+ summary: [],
251
+ details: {
252
+ fileCount: 1,
253
+ coverage: { implFiles: 1, testFiles: 0, missingTests: 0, issues: [] },
254
+ commits: {},
255
+ security: [
256
+ { type: 'console-log', severity: 'low', line: 'console.log("debug")' },
257
+ { type: 'todo-comment', severity: 'info', line: '// TODO: fix later' },
258
+ ],
259
+ },
260
+ };
261
+
262
+ const markdown = formatReviewMarkdown(report);
263
+
264
+ // Low severity issues should not appear in the Security Issues section
265
+ expect(markdown).not.toContain('Security Issues');
266
+ });
267
+ });
268
+ });