From 44019a7e372203415c55984b4171912ec6db1d37 Mon Sep 17 00:00:00 2001 From: ddjain Date: Thu, 30 Oct 2025 10:43:46 +0530 Subject: [PATCH 1/2] feat(rule): add test-missing, test-only-change, test-improvement (closes #7, #8, #9) --- __tests__/tests/test-improvement.test.js | 17 +++++++++++ __tests__/tests/test-missing.test.js | 17 +++++++++++ __tests__/tests/test-only-change.test.js | 17 +++++++++++ src/rules/tests/test-improvement.js | 39 ++++++++++++++++++++++++ src/rules/tests/test-missing.js | 39 ++++++++++++++++++++++++ src/rules/tests/test-only-change.js | 38 +++++++++++++++++++++++ 6 files changed, 167 insertions(+) create mode 100644 __tests__/tests/test-improvement.test.js create mode 100644 __tests__/tests/test-missing.test.js create mode 100644 __tests__/tests/test-only-change.test.js create mode 100644 src/rules/tests/test-improvement.js create mode 100644 src/rules/tests/test-missing.js create mode 100644 src/rules/tests/test-only-change.js diff --git a/__tests__/tests/test-improvement.test.js b/__tests__/tests/test-improvement.test.js new file mode 100644 index 0000000..6d3a5fa --- /dev/null +++ b/__tests__/tests/test-improvement.test.js @@ -0,0 +1,17 @@ +const rule = require('../../src/rules/tests/test-improvement'); + +describe('Test Improvement Rule', () => { + it('labels when both source and tests change', () => { + const files = [{ filename: 'src/app.ts' }, { filename: '__tests__/app.test.ts' }]; + const labels = rule({ files, pr: {}, enableDebug: false }); + expect(labels).toContain('test-improvement'); + }); + + it('does not label when only tests change', () => { + const files = [{ filename: '__tests__/app.test.ts' }]; + const labels = rule({ files, pr: {}, enableDebug: false }); + expect(labels).toEqual([]); + }); +}); + + diff --git a/__tests__/tests/test-missing.test.js b/__tests__/tests/test-missing.test.js new file mode 100644 index 0000000..5a7fa19 --- /dev/null +++ b/__tests__/tests/test-missing.test.js @@ -0,0 +1,17 @@ +const rule = require('../../src/rules/tests/test-missing'); + +describe('Test Missing Rule', () => { + it('labels when source changes without tests', () => { + const files = [{ filename: 'src/app.ts' }, { filename: 'src/util.ts' }]; + const labels = rule({ files, pr: {}, enableDebug: false }); + expect(labels).toContain('test-missing'); + }); + + it('does not label when tests are present', () => { + const files = [{ filename: 'src/app.ts' }, { filename: '__tests__/app.test.ts' }]; + const labels = rule({ files, pr: {}, enableDebug: false }); + expect(labels).toEqual([]); + }); +}); + + diff --git a/__tests__/tests/test-only-change.test.js b/__tests__/tests/test-only-change.test.js new file mode 100644 index 0000000..d90707e --- /dev/null +++ b/__tests__/tests/test-only-change.test.js @@ -0,0 +1,17 @@ +const rule = require('../../src/rules/tests/test-only-change'); + +describe('Test Only Change Rule', () => { + it('labels when only tests are changed', () => { + const files = [{ filename: '__tests__/a.test.js' }, { filename: 'tests/utils.spec.ts' }]; + const labels = rule({ files, pr: {}, enableDebug: false }); + expect(labels).toContain('test-only-change'); + }); + + it('does not label when source files also change', () => { + const files = [{ filename: '__tests__/a.test.js' }, { filename: 'src/app.ts' }]; + const labels = rule({ files, pr: {}, enableDebug: false }); + expect(labels).toEqual([]); + }); +}); + + diff --git a/src/rules/tests/test-improvement.js b/src/rules/tests/test-improvement.js new file mode 100644 index 0000000..5935a08 --- /dev/null +++ b/src/rules/tests/test-improvement.js @@ -0,0 +1,39 @@ +/** + * Test Improvement Detection Rule + * + * Adds `test-improvement` when both source and test files changed. + */ + +module.exports = function testImprovementRule({ files, pr, enableDebug }) { + const labels = []; + + const isTestFile = (name) => /(^|\/).__tests__\//i.test(name) || /\.(test|spec)\.[a-z0-9]+$/i.test(name) || /(^|\/)tests?\//i.test(name); + const isSourceFile = (name) => /\.(js|jsx|ts|tsx|java|go|py|rb|rs|php|cpp|c|cs|scala|kt|swift|vue|svelte)$/i.test(name) && !isTestFile(name); + + const names = (files || []).map(f => (f && f.filename ? String(f.filename).toLowerCase() : '')); + const hasSource = names.some(n => n && isSourceFile(n)); + const hasTests = names.some(n => n && isTestFile(n)); + + if (hasSource && hasTests) { + labels.push('test-improvement'); + } + + if (enableDebug) { + console.log(`[Test Improvement Rule] hasSource=${hasSource} hasTests=${hasTests} → ${labels.join(', ') || 'none'}`); + } + + return labels; +}; + +module.exports.metadata = { + name: 'Test Improvement Detection', + description: 'Detects PRs that modify tests alongside code changes', + labels: [ + { name: 'test-improvement', color: '0E8A16', description: 'Tests improved/added with code changes' } + ], + author: 'pr-auto-labeler', + version: '1.0.0', + category: 'tests' +}; + + diff --git a/src/rules/tests/test-missing.js b/src/rules/tests/test-missing.js new file mode 100644 index 0000000..a816ac5 --- /dev/null +++ b/src/rules/tests/test-missing.js @@ -0,0 +1,39 @@ +/** + * Source Changed Without Tests Detection Rule + * + * Adds `test-missing` when source files changed but no test files changed. + */ + +module.exports = function testMissingRule({ files, pr, enableDebug }) { + const labels = []; + + const isTestFile = (name) => /(^|\/)__tests__\//i.test(name) || /\.(test|spec)\.[a-z0-9]+$/i.test(name) || /(^|\/)tests?\//i.test(name); + const isSourceFile = (name) => /\.(js|jsx|ts|tsx|java|go|py|rb|rs|php|cpp|c|cs|scala|kt|swift|vue|svelte)$/i.test(name); + + const names = (files || []).map(f => (f && f.filename ? String(f.filename).toLowerCase() : '')); + const hasSource = names.some(n => n && isSourceFile(n)); + const hasTests = names.some(n => n && isTestFile(n)); + + if (hasSource && !hasTests) { + labels.push('test-missing'); + } + + if (enableDebug) { + console.log(`[Test Missing Rule] hasSource=${hasSource} hasTests=${hasTests} → ${labels.join(', ') || 'none'}`); + } + + return labels; +}; + +module.exports.metadata = { + name: 'Source Changed Without Tests', + description: 'Detects code changes without accompanying tests', + labels: [ + { name: 'test-missing', color: 'D93F0B', description: 'Code changes without tests' } + ], + author: 'pr-auto-labeler', + version: '1.0.0', + category: 'tests' +}; + + diff --git a/src/rules/tests/test-only-change.js b/src/rules/tests/test-only-change.js new file mode 100644 index 0000000..b563f9b --- /dev/null +++ b/src/rules/tests/test-only-change.js @@ -0,0 +1,38 @@ +/** + * Test-Only Change Detection Rule + * + * Adds `test-only-change` when only test files are changed. + */ + +module.exports = function testOnlyChangeRule({ files, pr, enableDebug }) { + const labels = []; + + const isTestFile = (name) => /(^|\/)__tests__\//i.test(name) || /\.(test|spec)\.[a-z0-9]+$/i.test(name) || /(^|\/)tests?\//i.test(name); + + const names = (files || []).map(f => (f && f.filename ? String(f.filename).toLowerCase() : '')); + const hasAny = names.length > 0; + const allAreTests = hasAny && names.every(n => n && isTestFile(n)); + + if (allAreTests) { + labels.push('test-only-change'); + } + + if (enableDebug) { + console.log(`[Test Only Change Rule] allAreTests=${allAreTests} → ${labels.join(', ') || 'none'}`); + } + + return labels; +}; + +module.exports.metadata = { + name: 'Test-only Change Detection', + description: 'Detects PRs that modify only test files', + labels: [ + { name: 'test-only-change', color: '0E8A16', description: 'Only test files changed' } + ], + author: 'pr-auto-labeler', + version: '1.0.0', + category: 'tests' +}; + + From 4129bab284ab06d8d14a4077ec947bcfd5831610 Mon Sep 17 00:00:00 2001 From: ddjain Date: Thu, 30 Oct 2025 11:54:30 +0530 Subject: [PATCH 2/2] test: improve test rules coverage to 85%+ --- __tests__/tests/test-improvement.test.js | 28 +++++++++++++++++++++++- __tests__/tests/test-missing.test.js | 28 +++++++++++++++++++++++- __tests__/tests/test-only-change.test.js | 22 ++++++++++++++++++- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/__tests__/tests/test-improvement.test.js b/__tests__/tests/test-improvement.test.js index 6d3a5fa..ffc91a1 100644 --- a/__tests__/tests/test-improvement.test.js +++ b/__tests__/tests/test-improvement.test.js @@ -7,11 +7,37 @@ describe('Test Improvement Rule', () => { expect(labels).toContain('test-improvement'); }); + it('labels for various source and test combinations', () => { + const files = [{ filename: 'lib/util.js' }, { filename: 'test/util.spec.js' }]; + const labels = rule({ files, pr: {}, enableDebug: false }); + expect(labels).toContain('test-improvement'); + }); + it('does not label when only tests change', () => { const files = [{ filename: '__tests__/app.test.ts' }]; const labels = rule({ files, pr: {}, enableDebug: false }); expect(labels).toEqual([]); }); -}); + it('does not label when only source changes', () => { + const files = [{ filename: 'src/app.ts' }]; + const labels = rule({ files, pr: {}, enableDebug: false }); + expect(labels).toEqual([]); + }); + it('does not label when only docs change', () => { + const files = [{ filename: 'README.md' }]; + const labels = rule({ files, pr: {}, enableDebug: false }); + expect(labels).toEqual([]); + }); + + it('handles empty files array', () => { + const labels = rule({ files: [], pr: {}, enableDebug: false }); + expect(labels).toEqual([]); + }); + + it('handles debug mode', () => { + const files = [{ filename: 'src/app.js' }, { filename: '__tests__/app.test.js' }]; + expect(() => rule({ files, pr: {}, enableDebug: true })).not.toThrow(); + }); +}); diff --git a/__tests__/tests/test-missing.test.js b/__tests__/tests/test-missing.test.js index 5a7fa19..6f69750 100644 --- a/__tests__/tests/test-missing.test.js +++ b/__tests__/tests/test-missing.test.js @@ -7,11 +7,37 @@ describe('Test Missing Rule', () => { expect(labels).toContain('test-missing'); }); + it('labels for various source file types', () => { + const files = [{ filename: 'lib/helper.js' }]; + const labels = rule({ files, pr: {}, enableDebug: false }); + expect(labels).toContain('test-missing'); + }); + it('does not label when tests are present', () => { const files = [{ filename: 'src/app.ts' }, { filename: '__tests__/app.test.ts' }]; const labels = rule({ files, pr: {}, enableDebug: false }); expect(labels).toEqual([]); }); -}); + it('does not label when only tests change', () => { + const files = [{ filename: '__tests__/app.test.ts' }]; + const labels = rule({ files, pr: {}, enableDebug: false }); + expect(labels).toEqual([]); + }); + it('does not label when only docs change', () => { + const files = [{ filename: 'README.md' }]; + const labels = rule({ files, pr: {}, enableDebug: false }); + expect(labels).toEqual([]); + }); + + it('handles empty files array', () => { + const labels = rule({ files: [], pr: {}, enableDebug: false }); + expect(labels).toEqual([]); + }); + + it('handles debug mode', () => { + const files = [{ filename: 'src/app.js' }]; + expect(() => rule({ files, pr: {}, enableDebug: true })).not.toThrow(); + }); +}); diff --git a/__tests__/tests/test-only-change.test.js b/__tests__/tests/test-only-change.test.js index d90707e..3c0ec9c 100644 --- a/__tests__/tests/test-only-change.test.js +++ b/__tests__/tests/test-only-change.test.js @@ -7,11 +7,31 @@ describe('Test Only Change Rule', () => { expect(labels).toContain('test-only-change'); }); + it('labels when test/ directory files change', () => { + const files = [{ filename: 'test/unit.js' }]; + const labels = rule({ files, pr: {}, enableDebug: false }); + expect(labels).toContain('test-only-change'); + }); + it('does not label when source files also change', () => { const files = [{ filename: '__tests__/a.test.js' }, { filename: 'src/app.ts' }]; const labels = rule({ files, pr: {}, enableDebug: false }); expect(labels).toEqual([]); }); -}); + it('does not label when only source files change', () => { + const files = [{ filename: 'src/app.js' }]; + const labels = rule({ files, pr: {}, enableDebug: false }); + expect(labels).toEqual([]); + }); + + it('handles empty files array', () => { + const labels = rule({ files: [], pr: {}, enableDebug: false }); + expect(labels).toEqual([]); + }); + it('handles debug mode', () => { + const files = [{ filename: '__tests__/test.js' }]; + expect(() => rule({ files, pr: {}, enableDebug: true })).not.toThrow(); + }); +});