collaboration-memory-service.test.ts
1 /** 2 * Unit Tests for CollaborationMemoryService 3 * 4 * Tests the core logic of tracking accepted/rejected commits. 5 * These tests focus on the pure logic functions that don't require file system access. 6 * 7 * For tests that involve file operations, see the integration tests in: 8 * src/features/social-resonance-filter/__tests__/collaboration/ 9 */ 10 11 import { describe, it, expect } from 'vitest'; 12 import { CollaborationMemoryService } from './collaboration-memory-service'; 13 14 describe('CollaborationMemoryService', () => { 15 16 describe('parseOriginalHash (static method)', () => { 17 it('should extract hash from cherry-pick message', () => { 18 const body = 'Some commit message\n\n(cherry picked from commit abc123def456)'; 19 const result = CollaborationMemoryService.parseOriginalHash(body); 20 expect(result).toBe('abc123def456'); 21 }); 22 23 it('should return null when no cherry-pick marker', () => { 24 const body = 'Regular commit message without cherry-pick info'; 25 const result = CollaborationMemoryService.parseOriginalHash(body); 26 expect(result).toBeNull(); 27 }); 28 29 it('should handle case-insensitive matching', () => { 30 const body = '(Cherry Picked From Commit abc123)'; 31 const result = CollaborationMemoryService.parseOriginalHash(body); 32 expect(result).toBe('abc123'); 33 }); 34 35 it('should handle full 40-character hashes', () => { 36 const fullHash = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2'; 37 const body = `(cherry picked from commit ${fullHash})`; 38 const result = CollaborationMemoryService.parseOriginalHash(body); 39 expect(result).toBe(fullHash); 40 }); 41 42 it('should handle hash in middle of message', () => { 43 const body = `Feature: Added new functionality 44 45 This is the body of the commit. 46 47 (cherry picked from commit deadbeef1234) 48 49 Signed-off-by: Someone`; 50 const result = CollaborationMemoryService.parseOriginalHash(body); 51 expect(result).toBe('deadbeef1234'); 52 }); 53 54 it('should handle short hashes', () => { 55 const body = '(cherry picked from commit abc1234)'; 56 const result = CollaborationMemoryService.parseOriginalHash(body); 57 expect(result).toBe('abc1234'); 58 }); 59 60 it('should return null for empty body', () => { 61 const result = CollaborationMemoryService.parseOriginalHash(''); 62 expect(result).toBeNull(); 63 }); 64 65 it('should return null for malformed cherry-pick message', () => { 66 const body = '(cherry picked from)'; // Missing commit hash 67 const result = CollaborationMemoryService.parseOriginalHash(body); 68 expect(result).toBeNull(); 69 }); 70 }); 71 72 describe('getEffectiveOriginalHash (static method)', () => { 73 it('should return cherry-picked hash when present in body', () => { 74 const hash = 'aabbccdd1234'; 75 const body = 'Message\n\n(cherry picked from commit deadbeef5678)'; 76 const result = CollaborationMemoryService.getEffectiveOriginalHash(hash, body); 77 expect(result).toBe('deadbeef5678'); 78 }); 79 80 it('should return the commit hash when no cherry-pick marker', () => { 81 const hash = 'abc123def789'; 82 const body = 'Regular commit message without cherry-pick info'; 83 const result = CollaborationMemoryService.getEffectiveOriginalHash(hash, body); 84 expect(result).toBe('abc123def789'); 85 }); 86 87 it('should return commit hash when body is empty', () => { 88 const hash = 'abc123'; 89 const result = CollaborationMemoryService.getEffectiveOriginalHash(hash, ''); 90 expect(result).toBe('abc123'); 91 }); 92 93 it('should handle real-world cherry-pick message format', () => { 94 const hash = '9f8e7d6c5b4a'; 95 const body = `Add collaboration memory service 96 97 Implements tracking of accepted/rejected commits per peer. 98 99 (cherry picked from commit 1a2b3c4d5e6f7890abcdef1234567890abcdef12)`; 100 const result = CollaborationMemoryService.getEffectiveOriginalHash(hash, body); 101 expect(result).toBe('1a2b3c4d5e6f7890abcdef1234567890abcdef12'); 102 }); 103 }); 104 105 describe('Hash deduplication scenarios', () => { 106 // These tests verify the logic used to deduplicate commits across relay chains 107 // Note: Git hashes are hexadecimal (0-9, a-f), so we use valid hex strings 108 109 it('should treat same original hash from different relays as same commit', () => { 110 // Scenario: Alice and Bob both relay Charlie's commit 111 const charliesOriginalHash = 'c0ffee123456'; 112 113 // Alice cherry-picks Charlie's commit 114 const alicesHash = 'a11ce0001234'; 115 const alicesBody = `Charlie's feature\n\n(cherry picked from commit ${charliesOriginalHash})`; 116 const fromAlice = CollaborationMemoryService.getEffectiveOriginalHash(alicesHash, alicesBody); 117 118 // Bob cherry-picks Charlie's commit 119 const bobsHash = 'b0b000005678'; 120 const bobsBody = `Charlie's feature\n\n(cherry picked from commit ${charliesOriginalHash})`; 121 const fromBob = CollaborationMemoryService.getEffectiveOriginalHash(bobsHash, bobsBody); 122 123 // Both should resolve to Charlie's original hash 124 expect(fromAlice).toBe(charliesOriginalHash); 125 expect(fromBob).toBe(charliesOriginalHash); 126 expect(fromAlice).toBe(fromBob); 127 }); 128 129 it('should distinguish original commits from relayed commits', () => { 130 const originalHash = 'deadbeef1234'; 131 132 // Original commit (no cherry-pick marker) 133 const directCommit = CollaborationMemoryService.getEffectiveOriginalHash( 134 originalHash, 135 'Some feature' 136 ); 137 138 // Relayed commit (has cherry-pick marker) 139 const relayedCommit = CollaborationMemoryService.getEffectiveOriginalHash( 140 'aabbccdd5678', 141 `Some feature\n\n(cherry picked from commit ${originalHash})` 142 ); 143 144 // Both should resolve to the same original hash 145 expect(directCommit).toBe(originalHash); 146 expect(relayedCommit).toBe(originalHash); 147 }); 148 149 it('should handle multi-hop relay chains', () => { 150 // Scenario: Charlie -> Alice -> Bob -> You 151 // Each cherry-pick preserves Charlie's original hash 152 153 const charliesHash = 'c0ffee000000'; 154 const alicesHash = 'a11ce0000000'; 155 const bobsHash = 'b0b000000000'; 156 157 // Alice cherry-picks from Charlie 158 const alicesBody = `Feature\n\n(cherry picked from commit ${charliesHash})`; 159 160 // Bob cherry-picks from Alice (but the message still has Charlie's hash) 161 // Because -x preserves the ORIGINAL hash, not Alice's hash 162 const bobsBody = `Feature\n\n(cherry picked from commit ${charliesHash})`; 163 164 const fromAlice = CollaborationMemoryService.getEffectiveOriginalHash(alicesHash, alicesBody); 165 const fromBob = CollaborationMemoryService.getEffectiveOriginalHash(bobsHash, bobsBody); 166 167 // Both trace back to Charlie 168 expect(fromAlice).toBe(charliesHash); 169 expect(fromBob).toBe(charliesHash); 170 }); 171 }); 172 173 describe('Memory file structure validation', () => { 174 // These tests document the expected structure of collaboration-memory.json 175 176 it('should document the expected empty structure', () => { 177 const emptyMemory = { 178 version: 1, 179 dreamNodes: {} 180 }; 181 182 expect(emptyMemory.version).toBe(1); 183 expect(emptyMemory.dreamNodes).toEqual({}); 184 }); 185 186 it('should document the expected structure with data', () => { 187 const memoryWithData = { 188 version: 1, 189 dreamNodes: { 190 'dreamnode-uuid-123': { 191 accepted: [ 192 { 193 originalHash: 'abc123', 194 appliedHash: 'def456', 195 relayedBy: ['peer-uuid-1', 'peer-uuid-2'], 196 subject: 'Add feature X', 197 acceptedAt: 1700000000000 198 } 199 ], 200 rejected: [ 201 { 202 originalHash: 'ghi789', 203 subject: 'Unwanted change', 204 rejectedAt: 1700000001000, 205 reason: 'Does not fit architecture' 206 } 207 ] 208 } 209 } 210 }; 211 212 // Validate structure 213 expect(memoryWithData.version).toBe(1); 214 expect(Object.keys(memoryWithData.dreamNodes)).toHaveLength(1); 215 216 const nodeState = memoryWithData.dreamNodes['dreamnode-uuid-123']; 217 expect(nodeState.accepted).toHaveLength(1); 218 expect(nodeState.rejected).toHaveLength(1); 219 220 // Accepted commit has required fields 221 const accepted = nodeState.accepted[0]; 222 expect(accepted.originalHash).toBeDefined(); 223 expect(accepted.appliedHash).toBeDefined(); 224 expect(accepted.relayedBy).toBeInstanceOf(Array); 225 expect(accepted.subject).toBeDefined(); 226 expect(accepted.acceptedAt).toBeTypeOf('number'); 227 228 // Rejected commit has required fields 229 const rejected = nodeState.rejected[0]; 230 expect(rejected.originalHash).toBeDefined(); 231 expect(rejected.subject).toBeDefined(); 232 expect(rejected.rejectedAt).toBeTypeOf('number'); 233 // reason is optional 234 expect(rejected.reason).toBeDefined(); 235 }); 236 }); 237 });