#!/usr/bin/env node /** * Unit tests for front-matter enrichment */ import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { enrichFrontmatterOnOpen, extractFrontmatter } from './ensureFrontmatter.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const TEST_DIR = path.join(__dirname, '..', 'test-vault-frontmatter'); // Test utilities async function setupTestDir() { await fs.mkdir(TEST_DIR, { recursive: true }); } async function cleanupTestDir() { try { await fs.rm(TEST_DIR, { recursive: true, force: true }); } catch (err) { console.warn('Cleanup warning:', err.message); } } async function createTestFile(filename, content) { const filePath = path.join(TEST_DIR, filename); await fs.writeFile(filePath, content, 'utf-8'); return filePath; } // Test runner const tests = []; function test(name, fn) { tests.push({ name, fn }); } // Tests test('Should add front-matter to file without any', async () => { const filePath = await createTestFile('no-frontmatter.md', '# Hello World\n\nThis is content.'); const result = await enrichFrontmatterOnOpen(filePath); if (!result.modified) { throw new Error('Expected file to be modified'); } const content = await fs.readFile(filePath, 'utf-8'); if (!content.startsWith('---\n')) { throw new Error('Expected front-matter to be added'); } if (!content.includes('titre: no-frontmatter')) { throw new Error('Expected titre to be set to filename'); } if (!content.includes('auteur: Bruno Charest')) { throw new Error('Expected auteur to be set'); } if (!content.includes('favoris: false')) { throw new Error('Expected favoris to be false'); } if (!content.includes('template: false')) { throw new Error('Expected template to be false'); } if (!content.includes('task: false')) { throw new Error('Expected task to be false'); } console.log('✓ Front-matter added successfully'); }); test('Should be idempotent (no changes on second run)', async () => { const filePath = await createTestFile('idempotent.md', '# Test\n\nContent here.'); // First enrichment const result1 = await enrichFrontmatterOnOpen(filePath); if (!result1.modified) { throw new Error('Expected first enrichment to modify file'); } const content1 = await fs.readFile(filePath, 'utf-8'); // Wait a bit to ensure timestamp would differ if modified await new Promise(resolve => setTimeout(resolve, 100)); // Second enrichment const result2 = await enrichFrontmatterOnOpen(filePath); if (result2.modified) { throw new Error('Expected second enrichment to NOT modify file (idempotent)'); } const content2 = await fs.readFile(filePath, 'utf-8'); if (content1 !== content2) { throw new Error('Expected content to be identical after second enrichment'); } console.log('✓ Idempotence verified'); }); test('Should preserve existing properties', async () => { const initialContent = `--- titre: Custom Title auteur: Bruno Charest custom_field: custom value favoris: true --- # Content`; const filePath = await createTestFile('preserve.md', initialContent); const result = await enrichFrontmatterOnOpen(filePath); const content = await fs.readFile(filePath, 'utf-8'); if (!content.includes('custom_field: custom value')) { throw new Error('Expected custom field to be preserved'); } if (!content.includes('favoris: true')) { throw new Error('Expected favoris: true to be preserved'); } if (!content.includes('titre: Custom Title')) { throw new Error('Expected custom title to be preserved'); } console.log('✓ Existing properties preserved'); }); test('Should maintain correct key order', async () => { const filePath = await createTestFile('order.md', '# Test\n\nContent.'); await enrichFrontmatterOnOpen(filePath); const content = await fs.readFile(filePath, 'utf-8'); // Extract front-matter const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); if (!fmMatch) { throw new Error('No front-matter found'); } const lines = fmMatch[1].split('\n').filter(l => l.trim()); // Check order of required keys const expectedOrder = [ 'titre:', 'auteur:', 'creation_date:', 'modification_date:', 'catégorie:', 'tags:', 'aliases:', 'status:', 'publish:', 'favoris:', 'template:', 'task:', 'archive:', 'draft:', 'private:' ]; let lastIndex = -1; for (const key of expectedOrder) { const currentIndex = lines.findIndex(l => l.startsWith(key)); if (currentIndex === -1) { throw new Error(`Expected key ${key} not found`); } if (currentIndex < lastIndex) { throw new Error(`Key order incorrect: ${key} appears before previous key`); } lastIndex = currentIndex; } console.log('✓ Key order is correct'); }); test('Should have no blank lines in front-matter', async () => { const filePath = await createTestFile('no-blanks.md', '# Test\n\nContent.'); await enrichFrontmatterOnOpen(filePath); const content = await fs.readFile(filePath, 'utf-8'); const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); if (!fmMatch) { throw new Error('No front-matter found'); } const fmContent = fmMatch[1]; if (fmContent.includes('\n\n')) { throw new Error('Found blank lines in front-matter'); } console.log('✓ No blank lines in front-matter'); }); test('Should use correct boolean types', async () => { const filePath = await createTestFile('booleans.md', '# Test\n\nContent.'); await enrichFrontmatterOnOpen(filePath); const fm = await extractFrontmatter(filePath); if (fm.favoris !== false) { throw new Error(`Expected favoris to be boolean false, got ${typeof fm.favoris}: ${fm.favoris}`); } if (fm.template !== false) { throw new Error(`Expected template to be boolean false, got ${typeof fm.template}: ${fm.template}`); } if (fm.task !== false) { throw new Error(`Expected task to be boolean false, got ${typeof fm.task}: ${fm.task}`); } if (fm.publish !== false) { throw new Error(`Expected publish to be boolean false, got ${typeof fm.publish}: ${fm.publish}`); } console.log('✓ Boolean types are correct'); }); test('Should use correct array types for tags and aliases', async () => { const filePath = await createTestFile('arrays.md', '# Test\n\nContent.'); await enrichFrontmatterOnOpen(filePath); const fm = await extractFrontmatter(filePath); if (!Array.isArray(fm.tags)) { throw new Error(`Expected tags to be array, got ${typeof fm.tags}`); } if (!Array.isArray(fm.aliases)) { throw new Error(`Expected aliases to be array, got ${typeof fm.aliases}`); } if (fm.tags.length !== 0) { throw new Error(`Expected tags to be empty array, got length ${fm.tags.length}`); } if (fm.aliases.length !== 0) { throw new Error(`Expected aliases to be empty array, got length ${fm.aliases.length}`); } console.log('✓ Array types are correct'); }); test('Should format dates in ISO 8601 with timezone', async () => { const filePath = await createTestFile('dates.md', '# Test\n\nContent.'); await enrichFrontmatterOnOpen(filePath); const fm = await extractFrontmatter(filePath); // Check creation_date format if (!fm.creation_date || typeof fm.creation_date !== 'string') { throw new Error('Expected creation_date to be a string'); } if (!fm.creation_date.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/)) { throw new Error(`creation_date format incorrect: ${fm.creation_date}`); } // Check modification_date format if (!fm.modification_date || typeof fm.modification_date !== 'string') { throw new Error('Expected modification_date to be a string'); } if (!fm.modification_date.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/)) { throw new Error(`modification_date format incorrect: ${fm.modification_date}`); } console.log('✓ Date formats are correct'); }); // Run all tests async function runTests() { console.log('\n🧪 Running front-matter enrichment tests...\n'); await setupTestDir(); let passed = 0; let failed = 0; for (const { name, fn } of tests) { try { await fn(); passed++; } catch (err) { console.error(`✗ ${name}`); console.error(` ${err.message}`); failed++; } } await cleanupTestDir(); console.log(`\n📊 Results: ${passed} passed, ${failed} failed\n`); if (failed > 0) { process.exit(1); } } // Execute if run directly if (process.argv[1] === __filename) { runTests().catch(err => { console.error('Test runner error:', err); process.exit(1); }); }