- Create Economist SubmissionTracking package correctly: * mainArticle = full blog post content * coverLetter = 216-word SIR— letter * Links to blog post via blogPostId - Archive 'Letter to The Economist' from blog posts (it's the cover letter) - Fix date display on article cards (use published_at) - Target publication already displaying via blue badge Database changes: - Make blogPostId optional in SubmissionTracking model - Economist package ID: 68fa85ae49d4900e7f2ecd83 - Le Monde package ID: 68fa2abd2e6acd5691932150 Next: Enhanced modal with tabs, validation, export 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
461 lines
16 KiB
JavaScript
461 lines
16 KiB
JavaScript
/**
|
|
* Unit Tests - ProhibitedTermsScanner
|
|
* Tests for proactive content scanning (Framework Phase 1)
|
|
*/
|
|
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const ProhibitedTermsScanner = require('../../scripts/framework-components/ProhibitedTermsScanner');
|
|
|
|
describe('ProhibitedTermsScanner', () => {
|
|
let scanner;
|
|
const testFilesDir = path.join(__dirname, '../tmp-scanner-test');
|
|
|
|
beforeEach(() => {
|
|
scanner = new ProhibitedTermsScanner({
|
|
silent: true,
|
|
basePath: testFilesDir // Only scan test directory
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Clean up test files
|
|
try {
|
|
await fs.rm(testFilesDir, { recursive: true, force: true });
|
|
} catch (err) {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
describe('Pattern Detection - inst_017 (Absolute Assurance)', () => {
|
|
test('should detect "guarantee" variations', async () => {
|
|
const testContent = `
|
|
This guarantees safety.
|
|
We guarantee results.
|
|
This is guaranteed to work.
|
|
We are guaranteeing success.
|
|
`;
|
|
|
|
// Create test file
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'test.md');
|
|
await fs.writeFile(testFile, testContent);
|
|
|
|
const violations = await scanner.scan();
|
|
const inst017Violations = violations.filter(v => v.rule === 'inst_017');
|
|
|
|
expect(inst017Violations.length).toBeGreaterThanOrEqual(4);
|
|
expect(inst017Violations.some(v => v.match.toLowerCase().includes('guarantee'))).toBe(true);
|
|
});
|
|
|
|
test('should detect "ensures 100%"', async () => {
|
|
const testContent = 'This ensures 100% accuracy.';
|
|
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'test.html');
|
|
await fs.writeFile(testFile, testContent);
|
|
|
|
const violations = await scanner.scan();
|
|
const inst017Violations = violations.filter(v => v.rule === 'inst_017');
|
|
|
|
expect(inst017Violations.length).toBeGreaterThan(0);
|
|
expect(inst017Violations.some(v => v.match.toLowerCase().includes('ensures'))).toBe(true);
|
|
});
|
|
|
|
test('should detect "eliminates all"', async () => {
|
|
const testContent = 'This eliminates all bugs.';
|
|
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'test.js');
|
|
await fs.writeFile(testFile, testContent);
|
|
|
|
const violations = await scanner.scan();
|
|
|
|
expect(violations.length).toBeGreaterThan(0);
|
|
expect(violations.some(v => v.match.toLowerCase().includes('eliminates'))).toBe(true);
|
|
});
|
|
|
|
test('should detect "never fails"', async () => {
|
|
const testContent = 'This system never fails.';
|
|
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'test.md');
|
|
await fs.writeFile(testFile, testContent);
|
|
|
|
const violations = await scanner.scan();
|
|
|
|
expect(violations.length).toBeGreaterThan(0);
|
|
expect(violations.some(v => v.match.toLowerCase().includes('never'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Pattern Detection - inst_018 (Unverified Claims)', () => {
|
|
test('should detect "production-ready" without context', async () => {
|
|
const testContent = 'This is production-ready software.';
|
|
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'test.md');
|
|
await fs.writeFile(testFile, testContent);
|
|
|
|
const violations = await scanner.scan();
|
|
const inst018Violations = violations.filter(v => v.rule === 'inst_018');
|
|
|
|
expect(inst018Violations.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('should detect "battle-tested"', async () => {
|
|
const testContent = 'Our battle-tested framework.';
|
|
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'test.html');
|
|
await fs.writeFile(testFile, testContent);
|
|
|
|
const violations = await scanner.scan();
|
|
|
|
expect(violations.some(v => v.match.toLowerCase().includes('battle-tested'))).toBe(true);
|
|
});
|
|
|
|
test('should allow "production-ready development tool"', async () => {
|
|
const testContent = 'Tractatus is a production-ready development tool.';
|
|
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'test.md');
|
|
await fs.writeFile(testFile, testContent);
|
|
|
|
const violations = await scanner.scan();
|
|
const inst018Violations = violations.filter(v =>
|
|
v.rule === 'inst_018' && v.match.toLowerCase().includes('production-ready')
|
|
);
|
|
|
|
expect(inst018Violations.length).toBe(0);
|
|
});
|
|
|
|
test('should allow "production-ready proof-of-concept"', async () => {
|
|
const testContent = 'This is a production-ready proof-of-concept.';
|
|
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'test.md');
|
|
await fs.writeFile(testFile, testContent);
|
|
|
|
const violations = await scanner.scan();
|
|
const inst018Violations = violations.filter(v =>
|
|
v.rule === 'inst_018' && v.match.toLowerCase().includes('production-ready')
|
|
);
|
|
|
|
expect(inst018Violations.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Context Awareness', () => {
|
|
test('should allow prohibited terms in comments about rules', async () => {
|
|
const testContent = `
|
|
// inst_017: Never use "guarantee"
|
|
// inst_017 prohibits guaranteed language
|
|
`;
|
|
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'test.js');
|
|
await fs.writeFile(testFile, testContent);
|
|
|
|
const violations = await scanner.scan();
|
|
|
|
// Should not flag violations in comments about the rules
|
|
expect(violations.length).toBe(0);
|
|
});
|
|
|
|
test('should exclude test files from scanning', async () => {
|
|
// Test files should be excluded by pattern
|
|
const scanner2 = new ProhibitedTermsScanner({ silent: true });
|
|
|
|
expect(scanner2.excludePatterns).toContain('**/tests/**/*.test.js');
|
|
expect(scanner2.excludePatterns).toContain('**/tests/**/*.spec.js');
|
|
});
|
|
|
|
test('should exclude GOVERNANCE-RULE-LIBRARY.md', async () => {
|
|
expect(scanner.excludePatterns).toContain('**/GOVERNANCE-RULE-LIBRARY.md');
|
|
});
|
|
|
|
test('should exclude case studies', async () => {
|
|
expect(scanner.excludePatterns).toContain('**/docs/case-studies/**');
|
|
});
|
|
});
|
|
|
|
describe('Suggestions', () => {
|
|
test('should suggest "enforcement" for "guarantee"', () => {
|
|
const suggestions = scanner.patterns.find(p => p.id === 'inst_017').suggestions;
|
|
|
|
expect(suggestions['guarantee']).toBe('enforcement');
|
|
expect(suggestions['guarantees']).toBe('enforces');
|
|
expect(suggestions['guaranteed']).toBe('enforced');
|
|
});
|
|
|
|
test('should suggest replacements for "ensures 100%"', () => {
|
|
const suggestions = scanner.patterns.find(p => p.id === 'inst_017').suggestions;
|
|
|
|
expect(suggestions['ensures 100%']).toBe('helps ensure');
|
|
});
|
|
|
|
test('should suggest replacements for inst_018 terms', () => {
|
|
const suggestions = scanner.patterns.find(p => p.id === 'inst_018').suggestions;
|
|
|
|
expect(suggestions['production-ready']).toBe('proof-of-concept');
|
|
expect(suggestions['battle-tested']).toBe('in development');
|
|
});
|
|
|
|
test('should get suggestion for matched term', () => {
|
|
const patternSet = scanner.patterns.find(p => p.id === 'inst_017');
|
|
const suggestion = scanner.getSuggestion('guarantee', patternSet.suggestions);
|
|
|
|
expect(suggestion).toBe('enforcement');
|
|
});
|
|
|
|
test('should handle case-insensitive matches', () => {
|
|
const patternSet = scanner.patterns.find(p => p.id === 'inst_017');
|
|
const suggestion = scanner.getSuggestion('GUARANTEE', patternSet.suggestions);
|
|
|
|
expect(suggestion).toBe('enforcement');
|
|
});
|
|
});
|
|
|
|
describe('Auto-fix Functionality', () => {
|
|
test('should fix simple violations', async () => {
|
|
const testContent = 'This guarantees safety.';
|
|
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'test.md');
|
|
await fs.writeFile(testFile, testContent);
|
|
|
|
const violations = await scanner.scan();
|
|
const results = await scanner.autoFix(violations);
|
|
|
|
expect(results.fixed).toBeGreaterThan(0);
|
|
|
|
const fixedContent = await fs.readFile(testFile, 'utf8');
|
|
expect(fixedContent).toContain('enforces'); // guarantees → enforces
|
|
expect(fixedContent).not.toContain('guarantees');
|
|
});
|
|
|
|
test('should preserve file structure during fix', async () => {
|
|
const testContent = `Line 1: Normal content
|
|
Line 2: This guarantees results
|
|
Line 3: More content
|
|
Line 4: This guaranteed safety
|
|
Line 5: Final line`;
|
|
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'test.md');
|
|
await fs.writeFile(testFile, testContent);
|
|
|
|
const violations = await scanner.scan();
|
|
await scanner.autoFix(violations);
|
|
|
|
const fixedContent = await fs.readFile(testFile, 'utf8');
|
|
const lines = fixedContent.split('\n');
|
|
|
|
expect(lines.length).toBe(5);
|
|
expect(lines[0]).toBe('Line 1: Normal content');
|
|
expect(lines[4]).toBe('Line 5: Final line');
|
|
});
|
|
|
|
test('should handle multiple violations in same file', async () => {
|
|
const testContent = `
|
|
We guarantee success.
|
|
This is guaranteed to work.
|
|
Our guarantees are strong.
|
|
`;
|
|
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'test.md');
|
|
await fs.writeFile(testFile, testContent);
|
|
|
|
const violations = await scanner.scan();
|
|
const results = await scanner.autoFix(violations);
|
|
|
|
expect(results.fixed).toBeGreaterThan(0);
|
|
|
|
const fixedContent = await fs.readFile(testFile, 'utf8');
|
|
expect(fixedContent).not.toContain('guarantee');
|
|
});
|
|
|
|
test('should skip violations without clear suggestions', async () => {
|
|
// inst_016 violations (fabricated statistics) require manual review
|
|
const testContent = '95% faster performance.';
|
|
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'test.md');
|
|
await fs.writeFile(testFile, testContent);
|
|
|
|
const violations = await scanner.scan();
|
|
const inst016Violations = violations.filter(v => v.rule === 'inst_016');
|
|
|
|
const results = await scanner.autoFix(inst016Violations);
|
|
|
|
// inst_016 requires source citation, can't auto-fix
|
|
expect(results.skipped).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('Formatting', () => {
|
|
test('should format empty violations list', () => {
|
|
const output = scanner.formatViolations([]);
|
|
|
|
expect(output).toContain('No prohibited terms found');
|
|
});
|
|
|
|
test('should format violations summary', () => {
|
|
const violations = [
|
|
{ rule: 'inst_017', file: 'test.md', line: 1, match: 'guarantee', severity: 'HIGH' },
|
|
{ rule: 'inst_017', file: 'test.md', line: 2, match: 'guaranteed', severity: 'HIGH' },
|
|
{ rule: 'inst_018', file: 'test.html', line: 5, match: 'battle-tested', severity: 'MEDIUM' }
|
|
];
|
|
|
|
const output = scanner.formatViolations(violations, false);
|
|
|
|
expect(output).toContain('Found 3 violation');
|
|
expect(output).toContain('inst_017: 2');
|
|
expect(output).toContain('inst_018: 1');
|
|
});
|
|
|
|
test('should format detailed violations', () => {
|
|
const violations = [
|
|
{
|
|
rule: 'inst_017',
|
|
file: 'test.md',
|
|
line: 1,
|
|
match: 'guarantee',
|
|
severity: 'HIGH',
|
|
context: 'This guarantees safety',
|
|
suggestion: 'enforcement'
|
|
}
|
|
];
|
|
|
|
const output = scanner.formatViolations(violations, true);
|
|
|
|
expect(output).toContain('test.md:1');
|
|
expect(output).toContain('Found: "guarantee"');
|
|
expect(output).toContain('Suggestion: enforcement');
|
|
});
|
|
});
|
|
|
|
describe('Utility Functions', () => {
|
|
test('should escape regex special characters', () => {
|
|
const escaped = scanner.escapeRegex('test.file[0]');
|
|
|
|
expect(escaped).toBe('test\\.file\\[0\\]');
|
|
});
|
|
|
|
test('should check allowed context for inst_017 references', () => {
|
|
const line = 'inst_017 prohibits guaranteed language';
|
|
const result = scanner.isAllowedContext(line, 'guaranteed', 'test.md');
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
test('should check allowed context for case studies', () => {
|
|
const line = 'This guarantees results';
|
|
const result = scanner.isAllowedContext(line, 'guarantees', 'docs/case-studies/example.md');
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
test('should reject prohibited terms in normal content', () => {
|
|
const line = 'This guarantees results';
|
|
const result = scanner.isAllowedContext(line, 'guarantees', 'README.md');
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('File Inclusion/Exclusion', () => {
|
|
test('should include markdown files', () => {
|
|
expect(scanner.includePatterns).toContain('**/*.md');
|
|
});
|
|
|
|
test('should include HTML files', () => {
|
|
expect(scanner.includePatterns).toContain('**/*.html');
|
|
});
|
|
|
|
test('should include JavaScript files', () => {
|
|
expect(scanner.includePatterns).toContain('**/*.js');
|
|
});
|
|
|
|
test('should exclude node_modules', () => {
|
|
expect(scanner.excludePatterns).toContain('**/node_modules/**');
|
|
});
|
|
|
|
test('should exclude .git directory', () => {
|
|
expect(scanner.excludePatterns).toContain('**/.git/**');
|
|
});
|
|
|
|
test('should exclude .claude directory', () => {
|
|
expect(scanner.excludePatterns).toContain('**/.claude/**');
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
test('should handle empty files', async () => {
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'empty.md');
|
|
await fs.writeFile(testFile, '');
|
|
|
|
const violations = await scanner.scan();
|
|
|
|
// Should not error on empty files
|
|
expect(Array.isArray(violations)).toBe(true);
|
|
});
|
|
|
|
test('should handle files with only whitespace', async () => {
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'whitespace.md');
|
|
await fs.writeFile(testFile, '\n\n \n\t\n');
|
|
|
|
const violations = await scanner.scan();
|
|
|
|
expect(Array.isArray(violations)).toBe(true);
|
|
});
|
|
|
|
test('should handle very long lines', async () => {
|
|
const longLine = 'a'.repeat(10000) + ' guarantee ' + 'b'.repeat(10000);
|
|
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'long.md');
|
|
await fs.writeFile(testFile, longLine);
|
|
|
|
const violations = await scanner.scan();
|
|
|
|
expect(violations.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('should handle violations at file end without newline', async () => {
|
|
const testContent = 'This guarantees results';
|
|
|
|
await fs.mkdir(testFilesDir, { recursive: true });
|
|
const testFile = path.join(testFilesDir, 'test.md');
|
|
await fs.writeFile(testFile, testContent);
|
|
|
|
const violations = await scanner.scan();
|
|
|
|
expect(violations.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('Silent Mode', () => {
|
|
test('should suppress console output in silent mode', async () => {
|
|
const silentScanner = new ProhibitedTermsScanner({
|
|
silent: true,
|
|
basePath: testFilesDir
|
|
});
|
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
|
|
await silentScanner.scan();
|
|
|
|
// In silent mode, should not call console.log for scanning message
|
|
const scanningCalls = consoleSpy.mock.calls.filter(call =>
|
|
call.some(arg => typeof arg === 'string' && arg.includes('Scanning'))
|
|
);
|
|
expect(scanningCalls.length).toBe(0);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
});
|