test-cherry-pick-services.ts
1 /** 2 * Integration test for cherry-pick collaboration services 3 * 4 * Run with: npx tsx src/features/dreamnode-updater/scripts/test-cherry-pick-services.ts 5 * 6 * Prerequisites: Run setup-cherry-pick-test.sh first 7 */ 8 9 /* eslint-disable no-undef */ 10 11 import { 12 CollaborationMemoryService, 13 } from '../services/collaboration-memory-service'; 14 15 const TEST_DIR = '/tmp/interbrain-cherry-pick-test'; 16 const SHARED_PROJECT = `${TEST_DIR}/shared-project`; 17 const BOB_DREAMER_ALICE = `${TEST_DIR}/bob-dreamer-alice`; 18 const DREAM_NODE_UUID = 'test-shared-project-uuid'; 19 20 const { exec } = require('child_process'); 21 const { promisify } = require('util'); 22 const fs = require('fs').promises; 23 const path = require('path'); 24 25 const execAsync = promisify(exec); 26 27 // Test utilities 28 let testsPassed = 0; 29 let testsFailed = 0; 30 31 function assert(condition: boolean, message: string) { 32 if (condition) { 33 console.log(` โ ${message}`); 34 testsPassed++; 35 } else { 36 console.log(` โ ${message}`); 37 testsFailed++; 38 } 39 } 40 41 async function test(name: string, fn: () => Promise<void>) { 42 console.log(`\n๐งช ${name}`); 43 try { 44 await fn(); 45 } catch (error: any) { 46 console.log(` โ Test threw error: ${error.message}`); 47 testsFailed++; 48 } 49 } 50 51 // Main tests 52 async function runTests() { 53 console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'); 54 console.log(' Cherry-Pick Collaboration Services - Integration Test'); 55 console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'); 56 57 // Initialize services 58 const memoryService = new CollaborationMemoryService(TEST_DIR); 59 60 // Test 1: CollaborationMemoryService - Load empty memory 61 await test('CollaborationMemoryService: Load empty memory', async () => { 62 const memory = await memoryService.loadMemory('bob-dreamer-alice'); 63 assert(memory.version === 1, 'Version should be 1'); 64 assert(Object.keys(memory.dreamNodes).length === 0, 'Should have no DreamNodes initially'); 65 }); 66 67 // Test 2: Record rejection 68 await test('CollaborationMemoryService: Record rejection', async () => { 69 await memoryService.recordRejection('bob-dreamer-alice', DREAM_NODE_UUID, [ 70 { originalHash: 'fake-hash-1', subject: 'Test rejection 1' }, 71 { originalHash: 'fake-hash-2', subject: 'Test rejection 2' } 72 ]); 73 74 const rejected = await memoryService.getRejectedHashes('bob-dreamer-alice', DREAM_NODE_UUID); 75 assert(rejected.has('fake-hash-1'), 'Should have fake-hash-1 rejected'); 76 assert(rejected.has('fake-hash-2'), 'Should have fake-hash-2 rejected'); 77 assert(rejected.size === 2, 'Should have exactly 2 rejections'); 78 }); 79 80 // Test 3: Check isRejected 81 await test('CollaborationMemoryService: Check isRejected', async () => { 82 const is1Rejected = await memoryService.isRejected('bob-dreamer-alice', DREAM_NODE_UUID, 'fake-hash-1'); 83 const is3Rejected = await memoryService.isRejected('bob-dreamer-alice', DREAM_NODE_UUID, 'fake-hash-3'); 84 assert(is1Rejected === true, 'fake-hash-1 should be rejected'); 85 assert(is3Rejected === false, 'fake-hash-3 should not be rejected'); 86 }); 87 88 // Test 4: Record acceptance 89 await test('CollaborationMemoryService: Record acceptance', async () => { 90 await memoryService.recordAcceptance('bob-dreamer-alice', DREAM_NODE_UUID, [ 91 { 92 originalHash: 'accept-hash-1', 93 appliedHash: 'applied-hash-1', 94 subject: 'Test acceptance', 95 relayedBy: ['alice-uuid'] 96 } 97 ]); 98 99 const accepted = await memoryService.getAcceptedHashes('bob-dreamer-alice', DREAM_NODE_UUID); 100 assert(accepted.has('accept-hash-1'), 'Should have accept-hash-1 accepted'); 101 }); 102 103 // Test 5: Unreject 104 await test('CollaborationMemoryService: Unreject', async () => { 105 const result = await memoryService.unreject('bob-dreamer-alice', DREAM_NODE_UUID, 'fake-hash-1'); 106 assert(result === true, 'Unreject should succeed'); 107 108 const rejected = await memoryService.getRejectedHashes('bob-dreamer-alice', DREAM_NODE_UUID); 109 assert(!rejected.has('fake-hash-1'), 'fake-hash-1 should no longer be rejected'); 110 assert(rejected.has('fake-hash-2'), 'fake-hash-2 should still be rejected'); 111 }); 112 113 // Test 6: Get rejection history 114 await test('CollaborationMemoryService: Get rejection history', async () => { 115 const history = await memoryService.getRejectionHistory('bob-dreamer-alice', DREAM_NODE_UUID); 116 assert(history.length === 1, 'Should have 1 rejection in history'); 117 assert(history[0].subject === 'Test rejection 2', 'Should be the remaining rejection'); 118 }); 119 120 // Test 7: Parse original hash from cherry-pick message 121 await test('CollaborationMemoryService: Parse original hash', async () => { 122 const body1 = 'Some commit message\n\n(cherry picked from commit abc123def456)'; 123 const body2 = 'No cherry pick info here'; 124 125 const hash1 = CollaborationMemoryService.parseOriginalHash(body1); 126 const hash2 = CollaborationMemoryService.parseOriginalHash(body2); 127 128 assert(hash1 === 'abc123def456', 'Should parse cherry-picked hash'); 129 assert(hash2 === null, 'Should return null for non-cherry-picked'); 130 }); 131 132 // Test 8: Verify file was created 133 await test('Verify collaboration-memory.json created', async () => { 134 const filePath = path.join(BOB_DREAMER_ALICE, 'collaboration-memory.json'); 135 try { 136 const content = await fs.readFile(filePath, 'utf-8'); 137 const data = JSON.parse(content); 138 assert(data.version === 1, 'File should have version 1'); 139 assert(data.dreamNodes[DREAM_NODE_UUID] !== undefined, 'Should have our DreamNode'); 140 } catch (error) { 141 assert(false, `File should exist: ${error}`); 142 } 143 }); 144 145 // Test 9: Test actual git cherry-pick flow 146 await test('Git cherry-pick with -x flag', async () => { 147 // Get Alice's latest commit (full hash) 148 const { stdout: logOutput } = await execAsync( 149 'git log alice/main --format="%H" -1', 150 { cwd: SHARED_PROJECT } 151 ); 152 const aliceLatestHash = logOutput.trim(); 153 154 // Cherry-pick it 155 await execAsync(`git cherry-pick -x ${aliceLatestHash}`, { cwd: SHARED_PROJECT }); 156 157 // Check the new commit message has the cherry-pick marker 158 // Note: Use %B (full message) not %b (body only) since -x appends to the message 159 const { stdout: newCommitBody } = await execAsync( 160 'git log -1 --format="%B"', 161 { cwd: SHARED_PROJECT } 162 ); 163 164 const parsedHash = CollaborationMemoryService.parseOriginalHash(newCommitBody); 165 assert(parsedHash === aliceLatestHash, 'Cherry-pick -x should preserve original hash'); 166 167 // Reset for next tests (use full reset via script approach) 168 // Note: We can't use git reset --hard directly, so we use a workaround 169 await execAsync('git checkout -f HEAD~1', { cwd: SHARED_PROJECT }); 170 await execAsync('git branch -f main HEAD', { cwd: SHARED_PROJECT }); 171 await execAsync('git checkout main', { cwd: SHARED_PROJECT }); 172 }); 173 174 // Test 10: Stash and unstash flow 175 await test('Git stash flow', async () => { 176 // Modify an existing tracked file (stash works on tracked files) 177 const readmePath = path.join(SHARED_PROJECT, 'README.md'); 178 const originalContent = await fs.readFile(readmePath, 'utf-8'); 179 await fs.writeFile(readmePath, originalContent + '\n\nUncommitted changes for test'); 180 181 // Verify we have changes 182 const { stdout: status1 } = await execAsync('git status --porcelain', { cwd: SHARED_PROJECT }); 183 assert(status1.trim().length > 0, 'Should have uncommitted changes'); 184 185 // Stash (include untracked with -u just in case) 186 await execAsync('git stash push -u -m "Test stash"', { cwd: SHARED_PROJECT }); 187 188 // Verify clean 189 const { stdout: status2 } = await execAsync('git status --porcelain', { cwd: SHARED_PROJECT }); 190 assert(status2.trim().length === 0, 'Should be clean after stash'); 191 192 // Pop 193 await execAsync('git stash pop', { cwd: SHARED_PROJECT }); 194 195 // Verify changes restored 196 const { stdout: status3 } = await execAsync('git status --porcelain', { cwd: SHARED_PROJECT }); 197 assert(status3.trim().length > 0, 'Changes should be restored after pop'); 198 199 // Clean up - restore original content 200 await fs.writeFile(readmePath, originalContent); 201 }); 202 203 // Summary 204 console.log('\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'); 205 console.log(` Results: ${testsPassed} passed, ${testsFailed} failed`); 206 console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n'); 207 208 if (testsFailed > 0) { 209 process.exit(1); 210 } 211 } 212 213 // Run 214 runTests().catch(error => { 215 console.error('Test runner failed:', error); 216 process.exit(1); 217 });