/** * Integration Test: File-to-Database Sync * Tests the dual governance architecture synchronization */ const fs = require('fs'); const path = require('path'); const mongoose = require('mongoose'); const { syncInstructions } = require('../../scripts/sync-instructions-to-db.js'); const GovernanceRule = require('../../src/models/GovernanceRule.model'); require('dotenv').config(); const INSTRUCTION_FILE = path.join(__dirname, '../../.claude/instruction-history.json'); const TEST_DB = 'tractatus_test_sync'; describe('Instruction Sync Integration Tests', () => { let originalDb; beforeAll(async () => { // Connect to test database const mongoUri = process.env.MONGODB_URI?.replace(/\/[^/]+$/, `/${TEST_DB}`) || `mongodb://localhost:27017/${TEST_DB}`; await mongoose.connect(mongoUri); originalDb = mongoose.connection.db.databaseName; }); afterAll(async () => { // Clean up test database await mongoose.connection.db.dropDatabase(); await mongoose.disconnect(); }); beforeEach(async () => { // Clear database before each test await GovernanceRule.deleteMany({}); }); describe('File Reading', () => { test('instruction file exists', () => { expect(fs.existsSync(INSTRUCTION_FILE)).toBe(true); }); test('instruction file is valid JSON', () => { const fileData = fs.readFileSync(INSTRUCTION_FILE, 'utf8'); expect(() => JSON.parse(fileData)).not.toThrow(); }); test('instruction file has expected structure', () => { const fileData = JSON.parse(fs.readFileSync(INSTRUCTION_FILE, 'utf8')); // v3 schema: version is under metadata expect(fileData).toHaveProperty('metadata'); expect(fileData.metadata).toHaveProperty('version'); expect(fileData).toHaveProperty('instructions'); expect(Array.isArray(fileData.instructions)).toBe(true); }); test('all instructions have required fields', () => { const fileData = JSON.parse(fs.readFileSync(INSTRUCTION_FILE, 'utf8')); fileData.instructions.forEach(inst => { expect(inst).toHaveProperty('id'); // v3 schema uses 'description' instead of 'text' expect(inst).toHaveProperty('description'); expect(inst).toHaveProperty('quadrant'); expect(inst).toHaveProperty('persistence'); }); }); }); describe('Initial Sync', () => { test('syncs all instructions from file to empty database', async () => { const result = await syncInstructions({ silent: true }); expect(result.success).toBe(true); expect(result.added).toBeGreaterThan(0); expect(result.updated).toBe(0); // First sync, nothing to update expect(result.finalCount).toBeGreaterThan(0); // Verify database has same count as file const fileData = JSON.parse(fs.readFileSync(INSTRUCTION_FILE, 'utf8')); const activeFileCount = fileData.instructions.filter(i => i.active !== false).length; expect(result.finalCount).toBe(activeFileCount); }); test('creates rules with correct schema', async () => { await syncInstructions({ silent: true }); const rules = await GovernanceRule.find({}).lean(); expect(rules.length).toBeGreaterThan(0); rules.forEach(rule => { // Required fields expect(rule).toHaveProperty('id'); expect(rule).toHaveProperty('text'); expect(rule).toHaveProperty('quadrant'); expect(rule).toHaveProperty('persistence'); expect(rule).toHaveProperty('source'); expect(rule).toHaveProperty('active'); // Source enum validation expect(['user_instruction', 'framework_default', 'automated', 'migration', 'claude_md_migration', 'test']) .toContain(rule.source); }); }); }); describe('Update Sync', () => { test('updates existing rules without duplicates', async () => { // First sync const result1 = await syncInstructions({ silent: true }); const count1 = result1.finalCount; // Second sync (should update, not add) const result2 = await syncInstructions({ silent: true }); expect(result2.success).toBe(true); expect(result2.added).toBe(0); // Nothing new to add expect(result2.updated).toBeGreaterThan(0); // All rules updated expect(result2.finalCount).toBe(count1); // Same active count }); test('preserves validation scores on update', async () => { // First sync await syncInstructions({ silent: true }); // Update a rule with validation scores const rule = await GovernanceRule.findOne({}); await GovernanceRule.findByIdAndUpdate(rule._id, { clarityScore: 85, specificityScore: 90, actionabilityScore: 80, validationStatus: 'VALIDATED', lastValidated: new Date() }); // Second sync await syncInstructions({ silent: true }); // Verify scores preserved const updatedRule = await GovernanceRule.findById(rule._id); expect(updatedRule.clarityScore).toBe(85); expect(updatedRule.specificityScore).toBe(90); expect(updatedRule.actionabilityScore).toBe(80); expect(updatedRule.validationStatus).toBe('VALIDATED'); }); }); describe('Orphan Handling', () => { test('deactivates rules not in file', async () => { // Create an orphan rule directly in DB await GovernanceRule.create({ id: 'test_orphan_001', text: 'This rule does not exist in the file', scope: 'PROJECT_SPECIFIC', applicableProjects: ['*'], quadrant: 'TACTICAL', persistence: 'MEDIUM', category: 'other', priority: 50, active: true, source: 'test', createdBy: 'test' }); // Sync from file const result = await syncInstructions({ silent: true }); expect(result.deactivated).toBe(1); // Verify orphan is inactive const orphan = await GovernanceRule.findOne({ id: 'test_orphan_001' }); expect(orphan.active).toBe(false); expect(orphan.notes).toContain('Deactivated during sync'); }); test.skip('exports orphans to backup file', async () => { // Create orphan await GovernanceRule.create({ id: 'test_orphan_002', text: 'Another orphan rule', scope: 'PROJECT_SPECIFIC', applicableProjects: ['*'], quadrant: 'TACTICAL', persistence: 'MEDIUM', category: 'other', priority: 50, active: true, source: 'test', createdBy: 'test' }); // Sync await syncInstructions({ silent: true }); // Check backup directory exists const backupDir = path.join(__dirname, '../../.claude/backups'); expect(fs.existsSync(backupDir)).toBe(true); // Check latest backup file contains orphan const backupFiles = fs.readdirSync(backupDir) .filter(f => f.startsWith('orphaned-rules-')) .sort() .reverse(); if (backupFiles.length > 0) { const latestBackup = JSON.parse( fs.readFileSync(path.join(backupDir, backupFiles[0]), 'utf8') ); expect(latestBackup.rules.some(r => r.id === 'test_orphan_002')).toBe(true); } }); }); describe('Source Mapping', () => { test('maps file source values to MongoDB enum values', async () => { // This test assumes there are instructions with different source values in the file await syncInstructions({ silent: true }); const rules = await GovernanceRule.find({}).lean(); // All sources should be valid enum values const validSources = ['user_instruction', 'framework_default', 'automated', 'migration', 'claude_md_migration', 'test']; rules.forEach(rule => { expect(validSources).toContain(rule.source); }); }); }); describe('Error Handling', () => { test('handles missing instruction file gracefully', async () => { // Temporarily rename file const tempFile = INSTRUCTION_FILE + '.tmp'; fs.renameSync(INSTRUCTION_FILE, tempFile); try { const result = await syncInstructions({ silent: true }); expect(result.success).toBe(false); expect(result.error).toBeDefined(); } finally { // Restore file fs.renameSync(tempFile, INSTRUCTION_FILE); } }); test('handles invalid JSON gracefully', async () => { // Temporarily replace file with invalid JSON const originalContent = fs.readFileSync(INSTRUCTION_FILE, 'utf8'); fs.writeFileSync(INSTRUCTION_FILE, 'INVALID JSON{{{'); try { const result = await syncInstructions({ silent: true }); expect(result.success).toBe(false); } finally { // Restore file fs.writeFileSync(INSTRUCTION_FILE, originalContent); } }); }); describe('Programmatic Options', () => { test('respects silent mode', async () => { const consoleSpy = jest.spyOn(console, 'log'); await syncInstructions({ silent: true }); // Silent mode should not log expect(consoleSpy).not.toHaveBeenCalled(); consoleSpy.mockRestore(); }); test.skip('dry run does not modify database', async () => { const result = await syncInstructions({ silent: true, dryRun: true }); expect(result.success).toBe(true); // Database should still be empty const count = await GovernanceRule.countDocuments({}); expect(count).toBe(0); }); }); describe('Idempotency', () => { test('multiple syncs produce same result', async () => { const result1 = await syncInstructions({ silent: true }); const result2 = await syncInstructions({ silent: true }); const result3 = await syncInstructions({ silent: true }); expect(result1.finalCount).toBe(result2.finalCount); expect(result2.finalCount).toBe(result3.finalCount); }); }); });