@theihtisham/ai-release-notes 1.0.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1493 -0
  3. package/__tests__/analyzer.test.js +63 -0
  4. package/__tests__/categorizer.test.js +93 -0
  5. package/__tests__/config.test.js +92 -0
  6. package/__tests__/formatter.test.js +63 -0
  7. package/__tests__/formatters.test.js +394 -0
  8. package/__tests__/migration.test.js +322 -0
  9. package/__tests__/semver.test.js +94 -0
  10. package/__tests__/tones.test.js +252 -0
  11. package/action.yml +113 -0
  12. package/index.js +73 -0
  13. package/jest.config.js +10 -0
  14. package/package.json +41 -0
  15. package/src/ai-writer.js +108 -0
  16. package/src/analyzer.js +232 -0
  17. package/src/analyzers/migration.js +355 -0
  18. package/src/categorizer.js +182 -0
  19. package/src/config.js +162 -0
  20. package/src/constants.js +137 -0
  21. package/src/contributor.js +144 -0
  22. package/src/diff-analyzer.js +202 -0
  23. package/src/formatter.js +336 -0
  24. package/src/formatters/discord.js +174 -0
  25. package/src/formatters/html.js +195 -0
  26. package/src/formatters/index.js +42 -0
  27. package/src/formatters/markdown.js +123 -0
  28. package/src/formatters/slack.js +176 -0
  29. package/src/formatters/twitter.js +242 -0
  30. package/src/formatters/types.js +48 -0
  31. package/src/generator.js +297 -0
  32. package/src/integrations/changelog.js +125 -0
  33. package/src/integrations/discord.js +96 -0
  34. package/src/integrations/github-release.js +75 -0
  35. package/src/integrations/indexer.js +119 -0
  36. package/src/integrations/slack.js +112 -0
  37. package/src/integrations/twitter.js +128 -0
  38. package/src/logger.js +52 -0
  39. package/src/prompts.js +210 -0
  40. package/src/rate-limiter.js +92 -0
  41. package/src/semver.js +129 -0
  42. package/src/tones/casual.js +114 -0
  43. package/src/tones/humorous.js +164 -0
  44. package/src/tones/index.js +38 -0
  45. package/src/tones/professional.js +125 -0
  46. package/src/tones/technical.js +164 -0
  47. package/src/tones/types.js +26 -0
@@ -0,0 +1,322 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ detectBreakingChanges,
5
+ generateMigrationGuide,
6
+ generateUpgradeCommand,
7
+ createBreakingChange,
8
+ extractType,
9
+ extractPRNumber,
10
+ extractMigrationGuide,
11
+ extractBeforeCode,
12
+ extractAfterCode,
13
+ extractAffectedAPIs,
14
+ detectAPISignatureChange,
15
+ } = require('../src/analyzers/migration');
16
+
17
+ /**
18
+ * Create a sample commit object.
19
+ */
20
+ function createCommit(message, sha = 'abc123') {
21
+ return { sha, commit: { message, author: { name: 'Dev', email: 'dev@test.com' } } };
22
+ }
23
+
24
+ describe('migration analyzer', () => {
25
+ describe('detectBreakingChanges', () => {
26
+ test('returns empty array for no commits', () => {
27
+ expect(detectBreakingChanges([])).toEqual([]);
28
+ expect(detectBreakingChanges(null)).toEqual([]);
29
+ });
30
+
31
+ test('detects BREAKING CHANGE footer', () => {
32
+ const commits = [
33
+ createCommit('feat: new API\n\nBREAKING CHANGE: old API removed'),
34
+ ];
35
+ const result = detectBreakingChanges(commits);
36
+ expect(result.length).toBe(1);
37
+ expect(result[0].description).toContain('old API removed');
38
+ });
39
+
40
+ test('detects ! suffix in conventional commits', () => {
41
+ const commits = [
42
+ createCommit('feat!: completely redesigned API'),
43
+ ];
44
+ const result = detectBreakingChanges(commits);
45
+ expect(result.length).toBe(1);
46
+ expect(result[0].description).toContain('completely redesigned API');
47
+ expect(result[0].severity).toBe('major');
48
+ });
49
+
50
+ test('detects "breaking:" prefix', () => {
51
+ const commits = [
52
+ createCommit('breaking: removed legacy support'),
53
+ ];
54
+ const result = detectBreakingChanges(commits);
55
+ expect(result.length).toBe(1);
56
+ });
57
+
58
+ test('detects "breaking change" keyword in message', () => {
59
+ const commits = [
60
+ createCommit('refactor: this is a breaking change to the config module'),
61
+ ];
62
+ const result = detectBreakingChanges(commits);
63
+ expect(result.length).toBe(1);
64
+ });
65
+
66
+ test('detects API signature changes from keywords', () => {
67
+ const commits = [
68
+ createCommit('refactor: remove getUserById method from UserService'),
69
+ ];
70
+ const result = detectBreakingChanges(commits);
71
+ expect(result.length).toBe(1);
72
+ expect(result[0].affected_apis).toContain('getUserById');
73
+ });
74
+
75
+ test('detects renamed APIs', () => {
76
+ const commits = [
77
+ createCommit('refactor: rename fetchUsers to getUsers'),
78
+ ];
79
+ const result = detectBreakingChanges(commits);
80
+ expect(result.length).toBe(1);
81
+ expect(result[0].affected_apis).toContain('fetchUsers');
82
+ });
83
+
84
+ test('detects deprecated APIs', () => {
85
+ const commits = [
86
+ createCommit('chore: deprecate Config.getOldValue'),
87
+ ];
88
+ const result = detectBreakingChanges(commits);
89
+ expect(result.length).toBe(1);
90
+ expect(result[0].affected_apis).toContain('Config.getOldValue');
91
+ });
92
+
93
+ test('extracts scope from conventional commit', () => {
94
+ const commits = [
95
+ createCommit('feat(api)!: redesigned endpoint structure'),
96
+ ];
97
+ const result = detectBreakingChanges(commits);
98
+ expect(result[0].scope).toBe('api');
99
+ });
100
+
101
+ test('extracts PR number', () => {
102
+ const commits = [
103
+ createCommit('feat!: big change (#123)'),
104
+ ];
105
+ const result = detectBreakingChanges(commits);
106
+ expect(result[0].pr).toBe(123);
107
+ });
108
+
109
+ test('detects multiple breaking changes', () => {
110
+ const commits = [
111
+ createCommit('feat!: first breaking change'),
112
+ createCommit('fix(api)!: second breaking change\n\nBREAKING CHANGE: details here'),
113
+ ];
114
+ const result = detectBreakingChanges(commits);
115
+ expect(result.length).toBe(2);
116
+ });
117
+
118
+ test('extracts migration guide from body', () => {
119
+ const commits = [
120
+ createCommit('feat!: changed API\n\nMigration Guide:\n- Step 1\n- Step 2\n- Step 3'),
121
+ ];
122
+ const result = detectBreakingChanges(commits);
123
+ expect(result[0].migration_guide).toContain('Step 1');
124
+ });
125
+
126
+ test('extracts before code blocks', () => {
127
+ const commits = [
128
+ createCommit('feat!: changed API\n\nBefore:\n```javascript\noldAPI()\n```\n\nAfter:\n```javascript\nnewAPI()\n```'),
129
+ ];
130
+ const result = detectBreakingChanges(commits);
131
+ expect(result[0].before_code).toContain('oldAPI()');
132
+ expect(result[0].after_code).toContain('newAPI()');
133
+ });
134
+ });
135
+
136
+ describe('generateMigrationGuide', () => {
137
+ test('returns no-migration message for empty changes', () => {
138
+ const result = generateMigrationGuide([]);
139
+ expect(result).toContain('No breaking changes detected');
140
+ });
141
+
142
+ test('generates structured migration guide', () => {
143
+ const changes = [
144
+ {
145
+ description: 'Removed /api/v1/users endpoint',
146
+ scope: 'api',
147
+ severity: 'major',
148
+ migration_guide: 'Use /api/v2/users instead',
149
+ pr: 42,
150
+ },
151
+ ];
152
+ const result = generateMigrationGuide(changes);
153
+
154
+ expect(result).toContain('Migration Guide');
155
+ expect(result).toContain('Removed /api/v1/users endpoint');
156
+ expect(result).toContain('Use /api/v2/users instead');
157
+ expect(result).toContain('PR #42');
158
+ });
159
+
160
+ test('includes before/after code examples', () => {
161
+ const changes = [
162
+ {
163
+ description: 'Changed function signature',
164
+ scope: 'utils',
165
+ before_code: 'oldFunction(arg1)',
166
+ after_code: 'newFunction(arg1, options)',
167
+ },
168
+ ];
169
+ const result = generateMigrationGuide(changes);
170
+
171
+ expect(result).toContain('oldFunction(arg1)');
172
+ expect(result).toContain('newFunction(arg1, options)');
173
+ });
174
+
175
+ test('generates default steps when no migration guide', () => {
176
+ const changes = [
177
+ {
178
+ description: 'Something broke',
179
+ scope: 'core',
180
+ },
181
+ ];
182
+ const result = generateMigrationGuide(changes);
183
+
184
+ expect(result).toContain('Migration Steps');
185
+ expect(result).toContain('Review the changes');
186
+ });
187
+
188
+ test('includes deprecation timeline', () => {
189
+ const changes = [
190
+ { description: 'A change', scope: 'api', severity: 'major' },
191
+ ];
192
+ const result = generateMigrationGuide(changes);
193
+
194
+ expect(result).toContain('Deprecation Timeline');
195
+ });
196
+
197
+ test('handles multiple breaking changes', () => {
198
+ const changes = [
199
+ { description: 'Change 1', scope: 'a', severity: 'major' },
200
+ { description: 'Change 2', scope: 'b', severity: 'major' },
201
+ ];
202
+ const result = generateMigrationGuide(changes);
203
+
204
+ expect(result).toContain('Change 1');
205
+ expect(result).toContain('Change 2');
206
+ });
207
+
208
+ test('lists affected APIs', () => {
209
+ const changes = [
210
+ {
211
+ description: 'API changed',
212
+ scope: 'api',
213
+ affected_apis: ['oldEndpoint()', 'Config.set()'],
214
+ },
215
+ ];
216
+ const result = generateMigrationGuide(changes);
217
+
218
+ expect(result).toContain('oldEndpoint()');
219
+ expect(result).toContain('Config.set()');
220
+ });
221
+ });
222
+
223
+ describe('generateUpgradeCommand', () => {
224
+ test('generates npm command', () => {
225
+ const result = generateUpgradeCommand('1.0.0', '2.0.0', 'my-package');
226
+ expect(result).toContain('npm install my-package@2.0.0');
227
+ });
228
+
229
+ test('generates yarn command', () => {
230
+ const result = generateUpgradeCommand('1.0.0', '2.0.0', 'my-package');
231
+ expect(result).toContain('yarn add my-package@2.0.0');
232
+ });
233
+
234
+ test('generates pnpm command', () => {
235
+ const result = generateUpgradeCommand('1.0.0', '2.0.0', 'my-package');
236
+ expect(result).toContain('pnpm add my-package@2.0.0');
237
+ });
238
+
239
+ test('warns about major upgrades', () => {
240
+ const result = generateUpgradeCommand('1.0.0', '2.0.0');
241
+ expect(result).toContain('major version upgrade');
242
+ });
243
+
244
+ test('notes minor version upgrades', () => {
245
+ const result = generateUpgradeCommand('1.0.0', '1.1.0');
246
+ expect(result).toContain('minor version');
247
+ expect(result).toContain('Backward compatible');
248
+ });
249
+
250
+ test('notes patch version upgrades', () => {
251
+ const result = generateUpgradeCommand('1.0.0', '1.0.1');
252
+ expect(result).toContain('patch upgrade');
253
+ expect(result).toContain('Safe to upgrade');
254
+ });
255
+ });
256
+
257
+ describe('helper functions', () => {
258
+ test('extractType returns correct type', () => {
259
+ expect(extractType('feat: something')).toBe('feat');
260
+ expect(extractType('fix: something')).toBe('fix');
261
+ expect(extractType('random message')).toBe('other');
262
+ });
263
+
264
+ test('extractPRNumber returns correct PR number', () => {
265
+ expect(extractPRNumber('feat: something (#42)')).toBe(42);
266
+ expect(extractPRNumber('no PR')).toBeNull();
267
+ });
268
+
269
+ test('extractMigrationGuide finds migration text', () => {
270
+ const body = 'Migration Guide:\n- Step 1\n- Step 2\n\nOther text';
271
+ expect(extractMigrationGuide(body)).toContain('Step 1');
272
+ });
273
+
274
+ test('extractMigrationGuide finds MIGRATION text', () => {
275
+ const body = 'MIGRATION:\n- Update code\n- Run tests';
276
+ expect(extractMigrationGuide(body)).toContain('Update code');
277
+ });
278
+
279
+ test('extractMigrationGuide returns empty for no guide', () => {
280
+ expect(extractMigrationGuide('no guide here')).toBe('');
281
+ expect(extractMigrationGuide('')).toBe('');
282
+ });
283
+
284
+ test('extractBeforeCode extracts code blocks', () => {
285
+ const body = 'Before:\n```javascript\noldCode()\n```\nAfter:\n```javascript\nnewCode()\n```';
286
+ expect(extractBeforeCode(body)).toContain('oldCode()');
287
+ });
288
+
289
+ test('extractAfterCode extracts second code block', () => {
290
+ const body = 'Before:\n```javascript\noldCode()\n```\nAfter:\n```javascript\nnewCode()\n```';
291
+ expect(extractAfterCode(body)).toContain('newCode()');
292
+ });
293
+
294
+ test('extractAfterCode returns empty for single code block', () => {
295
+ const body = '```javascript\nonlyCode()\n```';
296
+ expect(extractAfterCode(body)).toBe('');
297
+ });
298
+
299
+ test('extractAffectedAPIs finds API patterns', () => {
300
+ const body = 'Changed: `myApp.getUser()` and affected API: `myApp.setUser()`';
301
+ const apis = extractAffectedAPIs(body);
302
+ expect(apis.length).toBeGreaterThanOrEqual(2);
303
+ });
304
+
305
+ test('detectAPISignatureChange detects removed methods', () => {
306
+ expect(detectAPISignatureChange('remove getUserById method from UserService')).toBe('getUserById');
307
+ });
308
+
309
+ test('detectAPISignatureChange detects renamed APIs', () => {
310
+ expect(detectAPISignatureChange('rename fetchUsers to getUsers')).toBe('fetchUsers');
311
+ });
312
+
313
+ test('detectAPISignatureChange detects deprecated APIs', () => {
314
+ expect(detectAPISignatureChange('deprecate Config.getOldValue')).toBe('Config.getOldValue');
315
+ });
316
+
317
+ test('detectAPISignatureChange returns null for no match', () => {
318
+ expect(detectAPISignatureChange('add new feature')).toBeNull();
319
+ expect(detectAPISignatureChange(null)).toBeNull();
320
+ });
321
+ });
322
+ });
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ const { parseVersion, isPrerelease, getNextVersion, compareVersions } = require('../src/semver');
4
+
5
+ describe('semver', () => {
6
+ describe('parseVersion', () => {
7
+ test('parses valid semver', () => {
8
+ const result = parseVersion('1.2.3');
9
+ expect(result).toBeTruthy();
10
+ if (result) {
11
+ expect(result.major).toBe(1);
12
+ expect(result.minor).toBe(2);
13
+ expect(result.patch).toBe(3);
14
+ }
15
+ });
16
+
17
+ test('parses semver with v prefix', () => {
18
+ const result = parseVersion('v1.2.3');
19
+ expect(result).toBeTruthy();
20
+ if (result) {
21
+ expect(result.major).toBe(1);
22
+ }
23
+ });
24
+
25
+ test('parses prerelease version', () => {
26
+ const result = parseVersion('1.2.3-beta.1');
27
+ expect(result).toBeTruthy();
28
+ });
29
+
30
+ test('returns null for invalid version', () => {
31
+ expect(parseVersion('not-a-version')).toBeNull();
32
+ expect(parseVersion('')).toBeNull();
33
+ expect(parseVersion('1')).toBeNull();
34
+ });
35
+ });
36
+
37
+ describe('isPrerelease', () => {
38
+ test('identifies prerelease versions', () => {
39
+ expect(isPrerelease('1.0.0-alpha.1')).toBe(true);
40
+ expect(isPrerelease('1.0.0-beta')).toBe(true);
41
+ expect(isPrerelease('1.0.0-rc.2')).toBe(true);
42
+ });
43
+
44
+ test('identifies stable versions', () => {
45
+ expect(isPrerelease('1.0.0')).toBe(false);
46
+ expect(isPrerelease('2.3.4')).toBe(false);
47
+ });
48
+
49
+ test('handles v prefix', () => {
50
+ expect(isPrerelease('v1.0.0-alpha')).toBe(true);
51
+ expect(isPrerelease('v1.0.0')).toBe(false);
52
+ });
53
+ });
54
+
55
+ describe('compareVersions', () => {
56
+ test('returns positive for greater version', () => {
57
+ expect(compareVersions('2.0.0', '1.0.0')).toBeGreaterThan(0);
58
+ });
59
+
60
+ test('returns negative for lesser version', () => {
61
+ expect(compareVersions('1.0.0', '2.0.0')).toBeLessThan(0);
62
+ });
63
+
64
+ test('returns 0 for equal versions', () => {
65
+ expect(compareVersions('1.0.0', '1.0.0')).toBe(0);
66
+ });
67
+ });
68
+
69
+ describe('getNextVersion', () => {
70
+ test('bumps major for breaking changes', () => {
71
+ const commits = [
72
+ { commit: { message: 'feat: new api\n\nBREAKING CHANGE: old API removed' } },
73
+ ];
74
+ const result = getNextVersion(commits, '1.2.3');
75
+ expect(result).toMatch(/^2\.0\.0/);
76
+ });
77
+
78
+ test('bumps minor for features', () => {
79
+ const commits = [
80
+ { commit: { message: 'feat: new feature' } },
81
+ ];
82
+ const result = getNextVersion(commits, '1.2.3');
83
+ expect(result).toMatch(/^1\.3\.0/);
84
+ });
85
+
86
+ test('bumps patch for fixes', () => {
87
+ const commits = [
88
+ { commit: { message: 'fix: bug fix' } },
89
+ ];
90
+ const result = getNextVersion(commits, '1.2.3');
91
+ expect(result).toMatch(/^1\.2\.4/);
92
+ });
93
+ });
94
+ });
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ const { getTone, TONES, isValidTone, ProfessionalTone, CasualTone, HumorousTone, TechnicalTone } = require('../src/tones');
4
+
5
+ /**
6
+ * Create sample release data for testing.
7
+ */
8
+ function createSampleData(overrides = {}) {
9
+ return {
10
+ version: '2.1.0',
11
+ previousVersion: '2.0.0',
12
+ date: '2025-01-15',
13
+ summary: 'Release v2.1.0 with 3 features and 2 fixes.',
14
+ categories: {
15
+ '🚀 Features': [
16
+ { description: 'Added user avatar upload', type: 'feat', scope: 'api' },
17
+ { description: 'Added dark mode', type: 'feat', scope: 'ui' },
18
+ ],
19
+ '🐛 Bug Fixes': [
20
+ { description: 'Fixed login timeout', type: 'fix', scope: 'auth' },
21
+ ],
22
+ },
23
+ breaking: [
24
+ { description: 'Removed old API endpoint', scope: 'api', type: 'feat', migration_guide: 'Use new endpoint' },
25
+ ],
26
+ contributors: [
27
+ { login: 'alice', commits_count: 3, is_first_time: false },
28
+ { login: 'bob', commits_count: 1, is_first_time: true },
29
+ ],
30
+ repoUrl: 'https://github.com/acme/app',
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ describe('tones', () => {
36
+ describe('tone factory', () => {
37
+ test('getTone returns ProfessionalTone', () => {
38
+ const t = getTone('professional');
39
+ expect(t).toBeInstanceOf(ProfessionalTone);
40
+ expect(t.name).toBe('professional');
41
+ });
42
+
43
+ test('getTone returns CasualTone', () => {
44
+ const t = getTone('casual');
45
+ expect(t).toBeInstanceOf(CasualTone);
46
+ expect(t.name).toBe('casual');
47
+ });
48
+
49
+ test('getTone returns HumorousTone', () => {
50
+ const t = getTone('humorous');
51
+ expect(t).toBeInstanceOf(HumorousTone);
52
+ expect(t.name).toBe('humorous');
53
+ });
54
+
55
+ test('getTone returns TechnicalTone', () => {
56
+ const t = getTone('technical');
57
+ expect(t).toBeInstanceOf(TechnicalTone);
58
+ expect(t.name).toBe('technical');
59
+ });
60
+
61
+ test('getTone throws for unknown tone', () => {
62
+ expect(() => getTone('sarcastic')).toThrow('Unknown tone');
63
+ });
64
+
65
+ test('TONES contains all tone types', () => {
66
+ expect(TONES).toEqual(['professional', 'casual', 'humorous', 'technical']);
67
+ });
68
+
69
+ test('isValidTone validates correctly', () => {
70
+ expect(isValidTone('professional')).toBe(true);
71
+ expect(isValidTone('casual')).toBe(true);
72
+ expect(isValidTone('unknown')).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe('ProfessionalTone', () => {
77
+ test('applies professional language to summary', () => {
78
+ const data = createSampleData();
79
+ const tone = new ProfessionalTone();
80
+ const result = tone.apply(data);
81
+
82
+ expect(result.summary).toContain('We are pleased to announce');
83
+ expect(result.summary).toContain('2.1.0');
84
+ });
85
+
86
+ test('renames categories to professional names', () => {
87
+ const data = createSampleData();
88
+ const tone = new ProfessionalTone();
89
+ const result = tone.apply(data);
90
+
91
+ const categories = Object.keys(result.categories);
92
+ expect(categories.some(c => c.includes('New Features'))).toBe(true);
93
+ expect(categories.some(c => c.includes('Bug Fixes'))).toBe(true);
94
+ });
95
+
96
+ test('handles data without categories', () => {
97
+ const data = createSampleData({ categories: {} });
98
+ const tone = new ProfessionalTone();
99
+ const result = tone.apply(data);
100
+
101
+ expect(result.summary).toContain('2.1.0');
102
+ });
103
+ });
104
+
105
+ describe('CasualTone', () => {
106
+ test('applies casual language to summary', () => {
107
+ const data = createSampleData();
108
+ const tone = new CasualTone();
109
+ const result = tone.apply(data);
110
+
111
+ expect(result.summary).toContain("Here's what's new");
112
+ expect(result.summary).toContain('v2.1.0');
113
+ });
114
+
115
+ test('adds emojis to category names', () => {
116
+ const data = createSampleData();
117
+ const tone = new CasualTone();
118
+ const result = tone.apply(data);
119
+
120
+ const categories = Object.keys(result.categories);
121
+ expect(categories.some(c => c.includes(':rocket:') || c.includes(':bug:'))).toBe(true);
122
+ });
123
+
124
+ test('adds exclamation to feature descriptions', () => {
125
+ const data = createSampleData();
126
+ const tone = new CasualTone();
127
+ const result = tone.apply(data);
128
+
129
+ const featCategory = Object.keys(result.categories).find(c => c.includes('Cool'));
130
+ if (featCategory) {
131
+ const changes = result.categories[featCategory];
132
+ expect(changes.some(c => c.description.includes('!'))).toBe(true);
133
+ }
134
+ });
135
+
136
+ test('casual breaking changes include warning', () => {
137
+ const data = createSampleData();
138
+ const tone = new CasualTone();
139
+ const result = tone.apply(data);
140
+
141
+ expect(result.breaking[0].description).toContain('Watch out');
142
+ });
143
+ });
144
+
145
+ describe('HumorousTone', () => {
146
+ test('applies humorous language to summary', () => {
147
+ const data = createSampleData();
148
+ const tone = new HumorousTone();
149
+ const result = tone.apply(data);
150
+
151
+ expect(result.summary).toContain('v2.1.0');
152
+ // Should be funny - check for one of the known openings
153
+ const openings = ['Hold onto', 'Drumroll', 'Spoiler', 'Cue the', 'waiting for'];
154
+ expect(openings.some(o => result.summary.includes(o))).toBe(true);
155
+ });
156
+
157
+ test('uses humorous category names', () => {
158
+ const data = createSampleData();
159
+ const tone = new HumorousTone();
160
+ const result = tone.apply(data);
161
+
162
+ const categories = Object.keys(result.categories);
163
+ expect(categories.some(c => c.includes('New and Shiny') || c.includes('Bug Heaven'))).toBe(true);
164
+ });
165
+
166
+ test('adds humorous suffixes to feature descriptions', () => {
167
+ const data = createSampleData();
168
+ const tone = new HumorousTone();
169
+ const result = tone.apply(data);
170
+
171
+ const featCategory = Object.keys(result.categories).find(c => c.includes('Shiny'));
172
+ if (featCategory) {
173
+ const changes = result.categories[featCategory];
174
+ expect(changes.some(c =>
175
+ c.description.includes('love this') ||
176
+ c.description.includes('finally') ||
177
+ c.description.includes('really')
178
+ )).toBe(true);
179
+ }
180
+ });
181
+
182
+ test('adds funny prefixes to breaking changes', () => {
183
+ const data = createSampleData();
184
+ const tone = new HumorousTone();
185
+ const result = tone.apply(data);
186
+
187
+ const prefixes = ['Plot twist', 'Breaking news', 'Change of plans', 'In plot twist news'];
188
+ expect(prefixes.some(p => result.breaking[0].description.includes(p))).toBe(true);
189
+ });
190
+ });
191
+
192
+ describe('TechnicalTone', () => {
193
+ test('applies technical language to summary', () => {
194
+ const data = createSampleData();
195
+ const tone = new TechnicalTone();
196
+ const result = tone.apply(data);
197
+
198
+ expect(result.summary).toContain('v2.1.0');
199
+ expect(result.summary).toContain('MAJOR');
200
+ });
201
+
202
+ test('uses conventional commit prefixes in categories', () => {
203
+ const data = createSampleData();
204
+ const tone = new TechnicalTone();
205
+ const result = tone.apply(data);
206
+
207
+ const categories = Object.keys(result.categories);
208
+ expect(categories.some(c => c.startsWith('feat:') || c.startsWith('fix:'))).toBe(true);
209
+ });
210
+
211
+ test('includes scope in descriptions', () => {
212
+ const data = createSampleData();
213
+ const tone = new TechnicalTone();
214
+ const result = tone.apply(data);
215
+
216
+ const allChanges = Object.values(result.categories).flat();
217
+ expect(allChanges.some(c => c.description.includes('['))).toBe(true);
218
+ });
219
+
220
+ test('generates default migration guide for breaking changes', () => {
221
+ const data = createSampleData({
222
+ breaking: [{ description: 'Removed endpoint', scope: 'api', type: 'feat' }],
223
+ });
224
+ const tone = new TechnicalTone();
225
+ const result = tone.apply(data);
226
+
227
+ expect(result.breaking[0].migration_guide).toBeDefined();
228
+ expect(result.breaking[0].migration_guide).toContain('Migration Steps');
229
+ });
230
+
231
+ test('marks feature-only releases as MINOR', () => {
232
+ const data = createSampleData({ breaking: [] });
233
+ const tone = new TechnicalTone();
234
+ const result = tone.apply(data);
235
+
236
+ expect(result.summary).toContain('MINOR');
237
+ });
238
+
239
+ test('marks fix-only releases as PATCH', () => {
240
+ const data = createSampleData({
241
+ breaking: [],
242
+ categories: {
243
+ '🐛 Bug Fixes': [{ description: 'Fixed something', type: 'fix' }],
244
+ },
245
+ });
246
+ const tone = new TechnicalTone();
247
+ const result = tone.apply(data);
248
+
249
+ expect(result.summary).toContain('PATCH');
250
+ });
251
+ });
252
+ });