import { describe, it, expect, beforeEach, afterEach } from 'vitest';import { LessonsManager } from '../tools/prr/state/lessons.js';import * as fs from 'fs';import * as path from 'path'; describe('normalizeLessonText', () => { let lessonsManager: LessonsManager; const testDir = '/tmp/test-lessons-normalize'; beforeEach(() => { if (!fs.existsSync(testDir)) { fs.mkdirSync(testDir, { recursive: true }); } lessonsManager = new LessonsManager('test-owner', 'test-repo', 'test-branch'); }); afterEach(() => { if (fs.existsSync(testDir)) { fs.rmSync(testDir, { recursive: true, force: true }); } }); // Helper to access private method const normalize = (text: string): string | null => { return (lessonsManager as any).normalizeLessonText(text); }; describe('code fence removal', () => { it('removes code fences', () => { expect(normalize('```typescript\nsome code\n```')).toBeNull(); }); it('removes inline code backticks', () => { expect(normalize('use `const` instead')).toBeNull(); }); }); describe('markdown header removal', () => { it('removes markdown headers', () => { expect(normalize('### Important Note')).toBeNull(); }); it('removes multiple header levels', () => { expect(normalize('# Title')).toBeNull(); expect(normalize('## Subtitle')).toBeNull(); }); }); describe('bold line removal', () => { it('removes bold markers', () => { expect(normalize('**important text**')).toBeNull(); }); }); describe('list numbering and bullets', () => { it('removes numbered list prefixes', () => { expect(normalize('1. First item')).toBeNull(); expect(normalize('42. Some item')).toBeNull(); }); it('removes bullet points', () => { expect(normalize('- bullet item')).toBeNull(); expect(normalize('* star item')).toBeNull(); }); }); describe('comment token removal', () => { it('removes single-line comment tokens', () => { expect(normalize('// this is a comment')).toBeNull(); }); it('removes multi-line comment tokens', () => { expect(normalize('/* block comment */')).toBeNull(); expect(normalize('* continuation')).toBeNull(); }); }); describe('access modifier removal', () => { it('removes public/private/protected', () => { expect(normalize('public method()')).toBeNull(); expect(normalize('private field')).toBeNull(); expect(normalize('protected value')).toBeNull(); }); }); describe('declaration keyword removal', () => { it('removes class/interface/type keywords', () => { expect(normalize('class MyClass')).toBeNull(); expect(normalize('interface IFace')).toBeNull(); expect(normalize('type MyType')).toBeNull(); }); it('removes const/let/var keywords', () => { expect(normalize('const value')).toBeNull(); expect(normalize('let variable')).toBeNull(); expect(normalize('var old')).toBeNull(); }); it('removes import/export keywords', () => { expect(normalize('import something')).toBeNull(); expect(normalize('export default')).toBeNull(); }); }); describe('(inferred) removal', () => { it('removes (inferred) markers', () => { expect(normalize('(inferred) some text')).toBeNull(); expect(normalize('text (inferred) more')).toBeNull(); }); }); describe('file extension suffix stripping', () => { it('strips common file extensions from paths', () => { const result = normalize('Fix issue in file.ts'); expect(result).not.toContain('.ts:'); }); it('handles .js, .md, .json, .yml extensions', () => { expect(normalize('edit config.json carefully before release')).toBeTruthy(); expect(normalize('update readme.md with release steps')).toBeTruthy(); }); }); describe('trailing type/line-number patterns', () => { it('strips trailing line numbers like a:123', () => { const result = normalize('file.ts:123'); expect(result).toBeNull(); }); it('strips file:line patterns', () => { const result = normalize('src/index.ts:45'); expect(result).toBeNull(); }); }); describe('made no changes normalization', () => { it('normalizes "tool made no changes"', () => { const result = normalize('tool made no changes without explanation'); expect(result).toBe('tool made no changes without explanation'); }); it('normalizes "fixer made no changes"', () => { const result = normalize('fixer made no changes without explanation'); expect(result).toBe('fixer made no changes without explanation'); }); it('normalizes with "trying different approach"', () => { const result = normalize('tool made no changes - trying different approach'); expect(result).toBe('tool made no changes - trying different approach'); }); it('normalizes runner-prefixed variants', () => { const result = normalize('claude-code made no changes without explanation'); expect(result).toBe('tool made no changes without explanation'); }); it('collapses numeric prefixes', () => { const result = normalize('42 made no changes'); expect(result).toBe('tool made no changes'); }); }); describe('truncated and Fix for cases returning null', () => { it('returns null for "chars truncated"', () => { expect(normalize('500 chars truncated')).toBeNull(); expect(normalize('some text chars truncated here')).toBeNull(); }); it('returns null for "Fix for file:null"', () => { expect(normalize('Fix for src/file.ts:null')).toBeNull(); }); it('returns null for "Fix for file:undefined"', () => { expect(normalize('Fix for src/file.ts:undefined')).toBeNull(); }); it('returns null for "Fix for file:123"', () => { expect(normalize('Fix for src/file.ts:123')).toBeNull(); }); it('returns null for "Fix for file" without line', () => { expect(normalize('Fix for src/file.ts')).toBeNull(); }); it('returns null for bare numbers', () => { expect(normalize('123')).toBeNull(); expect(normalize('42.')).toBeNull(); }); }); describe('trimming and cleanup', () => { it('trims trailing colons', () => { const result = normalize('Always document decisions:'); expect(result).toBe('Always document decisions'); }); it('trims trailing dashes', () => { const result = normalize('Always document decisions -'); expect(result).toBe('Always document decisions'); }); it('returns null for empty result', () => { expect(normalize('')).toBeNull(); expect(normalize(' ')).toBeNull(); }); }); describe('combined/edge inputs', () => { it('handles multiple transformations in sequence', () => { const input = '### **1. // public class MyClass**'; const result = normalize(input); expect(result).toBeNull(); }); it('handles nested patterns', () => { const input = '```ts\n// private const value\n```'; const result = normalize(input); expect(result).toBeNull(); }); it('preserves meaningful content after all transformations', () => { const input = 'Always validate user input before processing'; const result = normalize(input); expect(result).toBe('Always validate user input before processing'); }); });});
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { LessonsManager } from '../tools/prr/state/lessons.js';
import * as fs from 'fs';
import * as path from 'path';
describe('normalizeLessonText', () => {
let lessonsManager: LessonsManager;
const testDir = '/tmp/test-lessons-normalize';
beforeEach(() => {
if (!fs.existsSync(testDir)) {
fs.mkdirSync(testDir, { recursive: true });
}
lessonsManager = new LessonsManager('test-owner', 'test-repo', 'test-branch');
});
afterEach(() => {
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
});
// Helper to access private method
const normalize = (text: string): string | null => {
return (lessonsManager as any).normalizeLessonText(text);
};
describe('code fence removal', () => {
it('removes code fences', () => {
expect(normalize('```typescript\nsome code\n```')).toBeNull();
});
it('removes inline code backticks', () => {
expect(normalize('use `const` instead')).toBeNull();
});
});
describe('markdown header removal', () => {
it('removes markdown headers', () => {
expect(normalize('### Important Note')).toBeNull();
});
it('removes multiple header levels', () => {
expect(normalize('# Title')).toBeNull();
expect(normalize('## Subtitle')).toBeNull();
});
});
describe('bold line removal', () => {
it('removes bold markers', () => {
expect(normalize('**important text**')).toBeNull();
});
});
describe('list numbering and bullets', () => {
it('removes numbered list prefixes', () => {
expect(normalize('1. First item')).toBeNull();
expect(normalize('42. Some item')).toBeNull();
});
it('removes bullet points', () => {
expect(normalize('- bullet item')).toBeNull();
expect(normalize('* star item')).toBeNull();
});
});
describe('comment token removal', () => {
it('removes single-line comment tokens', () => {
expect(normalize('// this is a comment')).toBeNull();
});
it('removes multi-line comment tokens', () => {
expect(normalize('/* block comment */')).toBeNull();
expect(normalize('* continuation')).toBeNull();
});
});
describe('access modifier removal', () => {
it('removes public/private/protected', () => {
expect(normalize('public method()')).toBeNull();
expect(normalize('private field')).toBeNull();
expect(normalize('protected value')).toBeNull();
});
});
describe('declaration keyword removal', () => {
it('removes class/interface/type keywords', () => {
expect(normalize('class MyClass')).toBeNull();
expect(normalize('interface IFace')).toBeNull();
expect(normalize('type MyType')).toBeNull();
});
it('removes const/let/var keywords', () => {
expect(normalize('const value')).toBeNull();
expect(normalize('let variable')).toBeNull();
expect(normalize('var old')).toBeNull();
});
it('removes import/export keywords', () => {
expect(normalize('import something')).toBeNull();
expect(normalize('export default')).toBeNull();
});
});
describe('(inferred) removal', () => {
it('removes (inferred) markers', () => {
expect(normalize('(inferred) some text')).toBeNull();
expect(normalize('text (inferred) more')).toBeNull();
});
});
describe('file extension suffix stripping', () => {
it('strips common file extensions from paths', () => {
const result = normalize('Fix issue in file.ts');
expect(result).not.toContain('.ts:');
});
it('handles .js, .md, .json, .yml extensions', () => {
expect(normalize('edit config.json carefully before release')).toBeTruthy();
expect(normalize('update readme.md with release steps')).toBeTruthy();
});
});
describe('trailing type/line-number patterns', () => {
it('strips trailing line numbers like a:123', () => {
const result = normalize('file.ts:123');
expect(result).toBeNull();
});
it('strips file:line patterns', () => {
const result = normalize('src/index.ts:45');
expect(result).toBeNull();
});
});
describe('made no changes normalization', () => {
it('normalizes "tool made no changes"', () => {
const result = normalize('tool made no changes without explanation');
expect(result).toBe('tool made no changes without explanation');
});
it('normalizes "fixer made no changes"', () => {
const result = normalize('fixer made no changes without explanation');
expect(result).toBe('fixer made no changes without explanation');
});
it('normalizes with "trying different approach"', () => {
const result = normalize('tool made no changes - trying different approach');
expect(result).toBe('tool made no changes - trying different approach');
});
it('normalizes runner-prefixed variants', () => {
const result = normalize('claude-code made no changes without explanation');
expect(result).toBe('tool made no changes without explanation');
});
it('collapses numeric prefixes', () => {
const result = normalize('42 made no changes');
expect(result).toBe('tool made no changes');
});
});
describe('truncated and Fix for cases returning null', () => {
it('returns null for "chars truncated"', () => {
expect(normalize('500 chars truncated')).toBeNull();
expect(normalize('some text chars truncated here')).toBeNull();
});
it('returns null for "Fix for file:null"', () => {
expect(normalize('Fix for src/file.ts:null')).toBeNull();
});
it('returns null for "Fix for file:undefined"', () => {
expect(normalize('Fix for src/file.ts:undefined')).toBeNull();
});
it('returns null for "Fix for file:123"', () => {
expect(normalize('Fix for src/file.ts:123')).toBeNull();
});
it('returns null for "Fix for file" without line', () => {
expect(normalize('Fix for src/file.ts')).toBeNull();
});
it('returns null for bare numbers', () => {
expect(normalize('123')).toBeNull();
expect(normalize('42.')).toBeNull();
});
});
describe('trimming and cleanup', () => {
it('trims trailing colons', () => {
const result = normalize('Always document decisions:');
expect(result).toBe('Always document decisions');
});
it('trims trailing dashes', () => {
const result = normalize('Always document decisions -');
expect(result).toBe('Always document decisions');
});
it('returns null for empty result', () => {
expect(normalize('')).toBeNull();
expect(normalize(' ')).toBeNull();
});
});
describe('combined/edge inputs', () => {
it('handles multiple transformations in sequence', () => {
const input = '### **1. // public class MyClass**';
const result = normalize(input);
expect(result).toBeNull();
});
it('handles nested patterns', () => {
const input = '```ts\n// private const value\n```';
const result = normalize(input);
expect(result).toBeNull();
});
it('preserves meaningful content after all transformations', () => {
const input = 'Always validate user input before processing';
const result = normalize(input);
expect(result).toBe('Always validate user input before processing');
});
});
});