/ src / features / dreamnode-updater / services / collaboration-memory-service.test.ts
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  });