/ src / features / dreamnode-updater / scripts / test-cherry-pick-services.ts
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  });