tractatus/tests/unit/ProhibitedTermsScanner.test.js
TheFlow ac2db33732 fix(submissions): restructure Economist package and fix article display
- 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>
2025-10-24 08:47:42 +13:00

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();
});
});
});