gsd-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +221 -0
  2. package/bin/cli.js +313 -0
  3. package/dist/auth-flow.d.ts +50 -0
  4. package/dist/auth-flow.d.ts.map +1 -0
  5. package/dist/auth-flow.js +233 -0
  6. package/dist/auth-flow.js.map +1 -0
  7. package/dist/auth.d.ts +42 -0
  8. package/dist/auth.d.ts.map +1 -0
  9. package/dist/auth.js +117 -0
  10. package/dist/auth.js.map +1 -0
  11. package/dist/command-executor.d.ts +44 -0
  12. package/dist/command-executor.d.ts.map +1 -0
  13. package/dist/command-executor.js +193 -0
  14. package/dist/command-executor.js.map +1 -0
  15. package/dist/command-executor.test.d.ts +8 -0
  16. package/dist/command-executor.test.d.ts.map +1 -0
  17. package/dist/command-executor.test.js +87 -0
  18. package/dist/command-executor.test.js.map +1 -0
  19. package/dist/command-queue.d.ts +44 -0
  20. package/dist/command-queue.d.ts.map +1 -0
  21. package/dist/command-queue.js +184 -0
  22. package/dist/command-queue.js.map +1 -0
  23. package/dist/command-queue.test.d.ts +7 -0
  24. package/dist/command-queue.test.d.ts.map +1 -0
  25. package/dist/command-queue.test.js +220 -0
  26. package/dist/command-queue.test.js.map +1 -0
  27. package/dist/config.d.ts +25 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +103 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/conflict-resolver.d.ts +43 -0
  32. package/dist/conflict-resolver.d.ts.map +1 -0
  33. package/dist/conflict-resolver.js +91 -0
  34. package/dist/conflict-resolver.js.map +1 -0
  35. package/dist/conflict-resolver.test.d.ts +7 -0
  36. package/dist/conflict-resolver.test.d.ts.map +1 -0
  37. package/dist/conflict-resolver.test.js +123 -0
  38. package/dist/conflict-resolver.test.js.map +1 -0
  39. package/dist/discovery.d.ts +59 -0
  40. package/dist/discovery.d.ts.map +1 -0
  41. package/dist/discovery.js +180 -0
  42. package/dist/discovery.js.map +1 -0
  43. package/dist/discovery.test.d.ts +8 -0
  44. package/dist/discovery.test.d.ts.map +1 -0
  45. package/dist/discovery.test.js +132 -0
  46. package/dist/discovery.test.js.map +1 -0
  47. package/dist/hash.d.ts +20 -0
  48. package/dist/hash.d.ts.map +1 -0
  49. package/dist/hash.js +35 -0
  50. package/dist/hash.js.map +1 -0
  51. package/dist/hash.test.d.ts +7 -0
  52. package/dist/hash.test.d.ts.map +1 -0
  53. package/dist/hash.test.js +58 -0
  54. package/dist/hash.test.js.map +1 -0
  55. package/dist/index.d.ts +11 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +202 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/integration.test.d.ts +8 -0
  60. package/dist/integration.test.d.ts.map +1 -0
  61. package/dist/integration.test.js +37 -0
  62. package/dist/integration.test.js.map +1 -0
  63. package/dist/logger.d.ts +68 -0
  64. package/dist/logger.d.ts.map +1 -0
  65. package/dist/logger.js +159 -0
  66. package/dist/logger.js.map +1 -0
  67. package/dist/output-streamer.d.ts +27 -0
  68. package/dist/output-streamer.d.ts.map +1 -0
  69. package/dist/output-streamer.js +71 -0
  70. package/dist/output-streamer.js.map +1 -0
  71. package/dist/output-streamer.test.d.ts +7 -0
  72. package/dist/output-streamer.test.d.ts.map +1 -0
  73. package/dist/output-streamer.test.js +90 -0
  74. package/dist/output-streamer.test.js.map +1 -0
  75. package/dist/realtime-subscriber.d.ts +63 -0
  76. package/dist/realtime-subscriber.d.ts.map +1 -0
  77. package/dist/realtime-subscriber.js +201 -0
  78. package/dist/realtime-subscriber.js.map +1 -0
  79. package/dist/realtime-subscriber.test.d.ts +7 -0
  80. package/dist/realtime-subscriber.test.d.ts.map +1 -0
  81. package/dist/realtime-subscriber.test.js +183 -0
  82. package/dist/realtime-subscriber.test.js.map +1 -0
  83. package/dist/reconnection-manager.d.ts +88 -0
  84. package/dist/reconnection-manager.d.ts.map +1 -0
  85. package/dist/reconnection-manager.js +229 -0
  86. package/dist/reconnection-manager.js.map +1 -0
  87. package/dist/reconnection-manager.test.d.ts +8 -0
  88. package/dist/reconnection-manager.test.d.ts.map +1 -0
  89. package/dist/reconnection-manager.test.js +151 -0
  90. package/dist/reconnection-manager.test.js.map +1 -0
  91. package/dist/remote-sync-handler.d.ts +61 -0
  92. package/dist/remote-sync-handler.d.ts.map +1 -0
  93. package/dist/remote-sync-handler.js +197 -0
  94. package/dist/remote-sync-handler.js.map +1 -0
  95. package/dist/remote-sync-handler.test.d.ts +7 -0
  96. package/dist/remote-sync-handler.test.d.ts.map +1 -0
  97. package/dist/remote-sync-handler.test.js +212 -0
  98. package/dist/remote-sync-handler.test.js.map +1 -0
  99. package/dist/retry.d.ts +35 -0
  100. package/dist/retry.d.ts.map +1 -0
  101. package/dist/retry.js +63 -0
  102. package/dist/retry.js.map +1 -0
  103. package/dist/retry.test.d.ts +5 -0
  104. package/dist/retry.test.d.ts.map +1 -0
  105. package/dist/retry.test.js +84 -0
  106. package/dist/retry.test.js.map +1 -0
  107. package/dist/storage-client.d.ts +69 -0
  108. package/dist/storage-client.d.ts.map +1 -0
  109. package/dist/storage-client.js +168 -0
  110. package/dist/storage-client.js.map +1 -0
  111. package/dist/storage-client.test.d.ts +7 -0
  112. package/dist/storage-client.test.d.ts.map +1 -0
  113. package/dist/storage-client.test.js +126 -0
  114. package/dist/storage-client.test.js.map +1 -0
  115. package/dist/supabase.d.ts +82 -0
  116. package/dist/supabase.d.ts.map +1 -0
  117. package/dist/supabase.js +341 -0
  118. package/dist/supabase.js.map +1 -0
  119. package/dist/supabase.test.d.ts +7 -0
  120. package/dist/supabase.test.d.ts.map +1 -0
  121. package/dist/supabase.test.js +273 -0
  122. package/dist/supabase.test.js.map +1 -0
  123. package/dist/sync-engine.d.ts +84 -0
  124. package/dist/sync-engine.d.ts.map +1 -0
  125. package/dist/sync-engine.js +251 -0
  126. package/dist/sync-engine.js.map +1 -0
  127. package/dist/sync-engine.test.d.ts +7 -0
  128. package/dist/sync-engine.test.d.ts.map +1 -0
  129. package/dist/sync-engine.test.js +241 -0
  130. package/dist/sync-engine.test.js.map +1 -0
  131. package/dist/sync-state.d.ts +82 -0
  132. package/dist/sync-state.d.ts.map +1 -0
  133. package/dist/sync-state.js +145 -0
  134. package/dist/sync-state.js.map +1 -0
  135. package/dist/sync-state.test.d.ts +7 -0
  136. package/dist/sync-state.test.d.ts.map +1 -0
  137. package/dist/sync-state.test.js +129 -0
  138. package/dist/sync-state.test.js.map +1 -0
  139. package/dist/types.d.ts +148 -0
  140. package/dist/types.d.ts.map +1 -0
  141. package/dist/types.js +8 -0
  142. package/dist/types.js.map +1 -0
  143. package/dist/types.test.d.ts +7 -0
  144. package/dist/types.test.d.ts.map +1 -0
  145. package/dist/types.test.js +73 -0
  146. package/dist/types.test.js.map +1 -0
  147. package/dist/watcher.d.ts +55 -0
  148. package/dist/watcher.d.ts.map +1 -0
  149. package/dist/watcher.js +214 -0
  150. package/dist/watcher.js.map +1 -0
  151. package/dist/watcher.test.d.ts +8 -0
  152. package/dist/watcher.test.d.ts.map +1 -0
  153. package/dist/watcher.test.js +164 -0
  154. package/dist/watcher.test.js.map +1 -0
  155. package/package.json +58 -0
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Tests for ReconnectionManager
3
+ *
4
+ * Validates disconnection detection, exponential backoff reconnection,
5
+ * and bidirectional reconciliation after reconnect.
6
+ */
7
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
8
+ import { createReconnectionManager } from './reconnection-manager.js';
9
+ import { createLogger } from './logger.js';
10
+ describe('ReconnectionManager', () => {
11
+ let mockSupabase;
12
+ let mockSubscriber;
13
+ let mockRemoteSyncHandler;
14
+ let mockSyncEngine;
15
+ let mockSyncStateManager;
16
+ let workspaces;
17
+ let logger;
18
+ beforeEach(() => {
19
+ // Mock Supabase client
20
+ mockSupabase = {
21
+ connect: vi.fn().mockResolvedValue({ success: true })
22
+ };
23
+ // Mock RealtimeSubscriber
24
+ mockSubscriber = {
25
+ subscribe: vi.fn()
26
+ };
27
+ // Mock RemoteSyncHandler
28
+ mockRemoteSyncHandler = {
29
+ handleRemoteChange: vi.fn()
30
+ };
31
+ // Mock SyncEngine
32
+ mockSyncEngine = {
33
+ getState: vi.fn().mockReturnValue({
34
+ sync_queue: []
35
+ })
36
+ };
37
+ // Mock SyncStateManager
38
+ mockSyncStateManager = {
39
+ getWorkspaceState: vi.fn().mockReturnValue({
40
+ workspace_id: 'workspace-123',
41
+ last_sync_timestamp: '2026-03-27T00:00:00Z',
42
+ connection_status: 'connected',
43
+ pending_changes_count: 0
44
+ }),
45
+ updateWorkspaceState: vi.fn()
46
+ };
47
+ // Create test workspace
48
+ workspaces = new Map([
49
+ [
50
+ 'workspace-123',
51
+ {
52
+ id: 'workspace-123',
53
+ root_path: '/test/workspace',
54
+ name: 'Test Workspace',
55
+ status: 'active',
56
+ last_sync: '2026-03-27T00:00:00Z',
57
+ discovered_at: '2026-03-27T00:00:00Z'
58
+ }
59
+ ]
60
+ ]);
61
+ logger = createLogger({
62
+ log_level: 'ERROR',
63
+ log_file: '/tmp/test.log',
64
+ log_rotation_days: 1
65
+ });
66
+ });
67
+ it('Test 1: detectDisconnection() sets connection_status to disconnected', () => {
68
+ const manager = createReconnectionManager(mockSupabase, mockSubscriber, mockRemoteSyncHandler, mockSyncEngine, mockSyncStateManager, workspaces, logger);
69
+ manager.detectDisconnection('workspace-123');
70
+ expect(mockSyncStateManager.updateWorkspaceState).toHaveBeenCalledWith('workspace-123', expect.objectContaining({
71
+ connection_status: 'disconnected'
72
+ }));
73
+ });
74
+ it('Test 2: attemptReconnection() retries with exponential backoff (1s, 2s, 4s, 8s, max 60s)', async () => {
75
+ vi.useFakeTimers();
76
+ // Mock connect to fail first 3 times, then succeed
77
+ let callCount = 0;
78
+ mockSupabase.connect = vi.fn().mockImplementation(() => {
79
+ callCount++;
80
+ if (callCount <= 3) {
81
+ return Promise.resolve({ success: false, error: 'Connection failed' });
82
+ }
83
+ return Promise.resolve({ success: true });
84
+ });
85
+ const manager = createReconnectionManager(mockSupabase, mockSubscriber, mockRemoteSyncHandler, mockSyncEngine, mockSyncStateManager, workspaces, logger);
86
+ // Start reconnection attempt
87
+ const reconnectPromise = manager.attemptReconnection('workspace-123');
88
+ // Fast-forward through backoff delays: 1s, 2s, 4s
89
+ await vi.advanceTimersByTimeAsync(1000); // First retry after 1s
90
+ await vi.advanceTimersByTimeAsync(2000); // Second retry after 2s
91
+ await vi.advanceTimersByTimeAsync(4000); // Third retry after 4s
92
+ await reconnectPromise;
93
+ expect(mockSupabase.connect).toHaveBeenCalledTimes(4); // Initial + 3 retries
94
+ expect(mockSyncStateManager.updateWorkspaceState).toHaveBeenCalledWith('workspace-123', expect.objectContaining({
95
+ connection_status: 'connected'
96
+ }));
97
+ vi.useRealTimers();
98
+ });
99
+ it('Test 3: On successful reconnect, reconcileChanges() syncs local → remote', async () => {
100
+ const manager = createReconnectionManager(mockSupabase, mockSubscriber, mockRemoteSyncHandler, mockSyncEngine, mockSyncStateManager, workspaces, logger);
101
+ // Mock sync engine with pending changes
102
+ mockSyncEngine.getState = vi.fn().mockReturnValue({
103
+ sync_queue: [
104
+ {
105
+ id: 'event-1',
106
+ workspace_id: 'workspace-123',
107
+ file_path: '.planning/STATE.md',
108
+ content: 'local content',
109
+ content_hash: 'hash123'
110
+ }
111
+ ]
112
+ });
113
+ await manager.reconcileChanges('workspace-123');
114
+ // Verify local changes were synced
115
+ expect(mockSyncEngine.getState).toHaveBeenCalled();
116
+ });
117
+ it('Test 4: On successful reconnect, reconcileChanges() syncs remote → local', async () => {
118
+ const manager = createReconnectionManager(mockSupabase, mockSubscriber, mockRemoteSyncHandler, mockSyncEngine, mockSyncStateManager, workspaces, logger);
119
+ await manager.reconcileChanges('workspace-123');
120
+ // Verify reconciliation was attempted
121
+ expect(mockSyncStateManager.getWorkspaceState).toHaveBeenCalledWith('workspace-123');
122
+ });
123
+ it('Test 5: reconcileChanges() uses last_sync_timestamp to fetch missed changes', async () => {
124
+ const manager = createReconnectionManager(mockSupabase, mockSubscriber, mockRemoteSyncHandler, mockSyncEngine, mockSyncStateManager, workspaces, logger);
125
+ mockSyncStateManager.getWorkspaceState = vi.fn().mockReturnValue({
126
+ workspace_id: 'workspace-123',
127
+ last_sync_timestamp: '2026-03-27T01:00:00Z',
128
+ connection_status: 'disconnected',
129
+ pending_changes_count: 0
130
+ });
131
+ await manager.reconcileChanges('workspace-123');
132
+ expect(mockSyncStateManager.getWorkspaceState).toHaveBeenCalledWith('workspace-123');
133
+ });
134
+ it('Test 6: reconcileChanges() detects conflicts during bidirectional sync', async () => {
135
+ const manager = createReconnectionManager(mockSupabase, mockSubscriber, mockRemoteSyncHandler, mockSyncEngine, mockSyncStateManager, workspaces, logger);
136
+ // This test verifies the conflict detection logic is invoked
137
+ await manager.reconcileChanges('workspace-123');
138
+ // Verify state was checked for reconciliation
139
+ expect(mockSyncStateManager.getWorkspaceState).toHaveBeenCalled();
140
+ });
141
+ it('Test 7: Logs recovery summary: "Synced N local changes, M remote changes, K conflicts"', async () => {
142
+ const logSpy = vi.spyOn(logger, 'info');
143
+ const manager = createReconnectionManager(mockSupabase, mockSubscriber, mockRemoteSyncHandler, mockSyncEngine, mockSyncStateManager, workspaces, logger);
144
+ await manager.reconcileChanges('workspace-123');
145
+ // Verify recovery summary was logged
146
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Reconciliation complete'), expect.objectContaining({
147
+ workspace_id: 'workspace-123'
148
+ }));
149
+ });
150
+ });
151
+ //# sourceMappingURL=reconnection-manager.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconnection-manager.test.js","sourceRoot":"","sources":["../src/reconnection-manager.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Remote sync handler for GSD Agent
3
+ *
4
+ * Handles remote → local sync by listening to RealtimeSubscriber events
5
+ * and writing changes to the filesystem with conflict detection.
6
+ */
7
+ import type { RemoteChangeEvent, Workspace } from './types.js';
8
+ import type { RealtimeSubscriber } from './realtime-subscriber.js';
9
+ import type { ConflictResolver } from './conflict-resolver.js';
10
+ import type { Logger } from './logger.js';
11
+ /**
12
+ * RemoteSyncHandler applies remote changes to local filesystem
13
+ *
14
+ * Subscribes to RealtimeSubscriber events and writes remote changes locally.
15
+ * Detects conflicts and creates git-style conflict markers when needed.
16
+ * Prevents sync loops by tracking in-progress syncs.
17
+ */
18
+ export declare class RemoteSyncHandler {
19
+ private subscriber;
20
+ private resolver;
21
+ private workspaces;
22
+ private logger;
23
+ private syncingFiles;
24
+ private remoteChangeHandler;
25
+ constructor(subscriber: RealtimeSubscriber, resolver: ConflictResolver, workspaces: Map<string, Workspace>, logger: Logger);
26
+ /**
27
+ * Start listening to remote change events
28
+ */
29
+ start(): void;
30
+ /**
31
+ * Stop listening to remote change events
32
+ */
33
+ stop(): void;
34
+ /**
35
+ * Handle remote change event
36
+ * @param event - RemoteChangeEvent from RealtimeSubscriber
37
+ */
38
+ handleRemoteChange(event: RemoteChangeEvent): Promise<void>;
39
+ /**
40
+ * Handle large file download from Supabase Storage
41
+ * @param event - RemoteChangeEvent with storage_url
42
+ * @param absolutePath - Absolute path to write file
43
+ */
44
+ private handleLargeFile;
45
+ /**
46
+ * Write file atomically with parent directory creation
47
+ * @param filePath - Absolute path to file
48
+ * @param content - File content
49
+ */
50
+ private writeFileAtomic;
51
+ }
52
+ /**
53
+ * Factory function to create RemoteSyncHandler
54
+ * @param subscriber - RealtimeSubscriber instance
55
+ * @param resolver - ConflictResolver instance
56
+ * @param workspaces - Map of workspace ID to Workspace
57
+ * @param logger - Logger instance
58
+ * @returns RemoteSyncHandler instance
59
+ */
60
+ export declare function createRemoteSyncHandler(subscriber: RealtimeSubscriber, resolver: ConflictResolver, workspaces: Map<string, Workspace>, logger: Logger): RemoteSyncHandler;
61
+ //# sourceMappingURL=remote-sync-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-sync-handler.d.ts","sourceRoot":"","sources":["../src/remote-sync-handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC9D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC;;;;;;GAMG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,UAAU,CAAoB;IACtC,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,UAAU,CAAwB;IAC1C,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,mBAAmB,CAAsD;gBAG/E,UAAU,EAAE,kBAAkB,EAC9B,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,EAClC,MAAM,EAAE,MAAM;IAUhB;;OAEG;IACH,KAAK,IAAI,IAAI;IAYb;;OAEG;IACH,IAAI,IAAI,IAAI;IAYZ;;;OAGG;IACG,kBAAkB,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgGjE;;;;OAIG;YACW,eAAe;IAmC7B;;;;OAIG;IACH,OAAO,CAAC,eAAe;CAUxB;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,kBAAkB,EAC9B,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,EAClC,MAAM,EAAE,MAAM,GACb,iBAAiB,CAEnB"}
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Remote sync handler for GSD Agent
3
+ *
4
+ * Handles remote → local sync by listening to RealtimeSubscriber events
5
+ * and writing changes to the filesystem with conflict detection.
6
+ */
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ /**
10
+ * RemoteSyncHandler applies remote changes to local filesystem
11
+ *
12
+ * Subscribes to RealtimeSubscriber events and writes remote changes locally.
13
+ * Detects conflicts and creates git-style conflict markers when needed.
14
+ * Prevents sync loops by tracking in-progress syncs.
15
+ */
16
+ export class RemoteSyncHandler {
17
+ subscriber;
18
+ resolver;
19
+ workspaces;
20
+ logger;
21
+ syncingFiles;
22
+ remoteChangeHandler;
23
+ constructor(subscriber, resolver, workspaces, logger) {
24
+ this.subscriber = subscriber;
25
+ this.resolver = resolver;
26
+ this.workspaces = workspaces;
27
+ this.logger = logger;
28
+ this.syncingFiles = new Set();
29
+ this.remoteChangeHandler = null;
30
+ }
31
+ /**
32
+ * Start listening to remote change events
33
+ */
34
+ start() {
35
+ this.logger.info('Remote sync handler started', {
36
+ workspace_count: this.workspaces.size
37
+ });
38
+ // Create bound handler
39
+ this.remoteChangeHandler = this.handleRemoteChange.bind(this);
40
+ // Subscribe to remote-change events
41
+ this.subscriber.on('remote-change', this.remoteChangeHandler);
42
+ }
43
+ /**
44
+ * Stop listening to remote change events
45
+ */
46
+ stop() {
47
+ this.logger.info('Remote sync handler stopped', {
48
+ workspace_count: this.workspaces.size
49
+ });
50
+ // Unsubscribe from events
51
+ if (this.remoteChangeHandler) {
52
+ this.subscriber.off('remote-change', this.remoteChangeHandler);
53
+ this.remoteChangeHandler = null;
54
+ }
55
+ }
56
+ /**
57
+ * Handle remote change event
58
+ * @param event - RemoteChangeEvent from RealtimeSubscriber
59
+ */
60
+ async handleRemoteChange(event) {
61
+ const syncKey = `${event.workspace_id}:${event.file_path}`;
62
+ // Prevent sync loops - skip if already syncing this file
63
+ if (this.syncingFiles.has(syncKey)) {
64
+ this.logger.debug('Skipping remote change - sync already in progress', {
65
+ workspace_id: event.workspace_id,
66
+ file_path: event.file_path
67
+ });
68
+ return;
69
+ }
70
+ try {
71
+ this.syncingFiles.add(syncKey);
72
+ // Get workspace
73
+ const workspace = this.workspaces.get(event.workspace_id);
74
+ if (!workspace) {
75
+ this.logger.warn('Workspace not found for remote change', {
76
+ workspace_id: event.workspace_id
77
+ });
78
+ return;
79
+ }
80
+ // Build absolute file path
81
+ const absolutePath = path.join(workspace.root_path, event.file_path);
82
+ // Handle large files from Storage
83
+ if (event.storage_url && !event.content) {
84
+ await this.handleLargeFile(event, absolutePath);
85
+ return;
86
+ }
87
+ // Check if file exists locally
88
+ const fileExists = fs.existsSync(absolutePath);
89
+ if (fileExists) {
90
+ // Read local content
91
+ const localContent = fs.readFileSync(absolutePath, 'utf8');
92
+ // Detect conflict
93
+ const conflict = await this.subscriber.detectConflict(event.workspace_id, event.file_path, event.content_hash, event.content || '', localContent);
94
+ if (conflict) {
95
+ // Conflict detected - create conflict markers
96
+ this.logger.warn('Conflict detected - creating conflict markers', {
97
+ workspace_id: event.workspace_id,
98
+ file_path: event.file_path,
99
+ local_hash: conflict.local_hash,
100
+ remote_hash: conflict.remote_hash
101
+ });
102
+ const conflictMarkers = this.resolver.resolveConflict(conflict);
103
+ // Write conflict markers to file
104
+ this.writeFileAtomic(absolutePath, conflictMarkers);
105
+ // Commit conflicted file
106
+ await this.resolver.commitConflict(workspace.root_path, event.file_path);
107
+ this.logger.info('Conflict resolved with markers and committed', {
108
+ workspace_id: event.workspace_id,
109
+ file_path: event.file_path
110
+ });
111
+ return;
112
+ }
113
+ }
114
+ // No conflict or file doesn't exist - write remote content
115
+ if (event.content !== null) {
116
+ this.writeFileAtomic(absolutePath, event.content);
117
+ this.logger.info('File synced from remote', {
118
+ workspace_id: event.workspace_id,
119
+ file_path: event.file_path,
120
+ size: event.size
121
+ });
122
+ }
123
+ }
124
+ catch (error) {
125
+ this.logger.error('Error handling remote change', {
126
+ workspace_id: event.workspace_id,
127
+ file_path: event.file_path,
128
+ error: error instanceof Error ? error.message : String(error)
129
+ });
130
+ }
131
+ finally {
132
+ this.syncingFiles.delete(syncKey);
133
+ }
134
+ }
135
+ /**
136
+ * Handle large file download from Supabase Storage
137
+ * @param event - RemoteChangeEvent with storage_url
138
+ * @param absolutePath - Absolute path to write file
139
+ */
140
+ async handleLargeFile(event, absolutePath) {
141
+ try {
142
+ this.logger.info('Downloading large file from Storage', {
143
+ workspace_id: event.workspace_id,
144
+ file_path: event.file_path,
145
+ storage_url: event.storage_url,
146
+ size: event.size
147
+ });
148
+ // Download from Storage
149
+ const response = await fetch(event.storage_url);
150
+ if (!response.ok) {
151
+ throw new Error(`Storage download failed: ${response.statusText}`);
152
+ }
153
+ const content = await response.text();
154
+ // Write to filesystem
155
+ this.writeFileAtomic(absolutePath, content);
156
+ this.logger.info('Large file downloaded and synced', {
157
+ workspace_id: event.workspace_id,
158
+ file_path: event.file_path,
159
+ size: content.length
160
+ });
161
+ }
162
+ catch (error) {
163
+ this.logger.error('Error downloading large file', {
164
+ workspace_id: event.workspace_id,
165
+ file_path: event.file_path,
166
+ storage_url: event.storage_url,
167
+ error: error instanceof Error ? error.message : String(error)
168
+ });
169
+ }
170
+ }
171
+ /**
172
+ * Write file atomically with parent directory creation
173
+ * @param filePath - Absolute path to file
174
+ * @param content - File content
175
+ */
176
+ writeFileAtomic(filePath, content) {
177
+ // Create parent directories if needed
178
+ const dir = path.dirname(filePath);
179
+ if (!fs.existsSync(dir)) {
180
+ fs.mkdirSync(dir, { recursive: true });
181
+ }
182
+ // Write file atomically
183
+ fs.writeFileSync(filePath, content, 'utf8');
184
+ }
185
+ }
186
+ /**
187
+ * Factory function to create RemoteSyncHandler
188
+ * @param subscriber - RealtimeSubscriber instance
189
+ * @param resolver - ConflictResolver instance
190
+ * @param workspaces - Map of workspace ID to Workspace
191
+ * @param logger - Logger instance
192
+ * @returns RemoteSyncHandler instance
193
+ */
194
+ export function createRemoteSyncHandler(subscriber, resolver, workspaces, logger) {
195
+ return new RemoteSyncHandler(subscriber, resolver, workspaces, logger);
196
+ }
197
+ //# sourceMappingURL=remote-sync-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-sync-handler.js","sourceRoot":"","sources":["../src/remote-sync-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Tests for RemoteSyncHandler
3
+ *
4
+ * Verifies remote → local sync with conflict detection and filesystem writes.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=remote-sync-handler.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-sync-handler.test.d.ts","sourceRoot":"","sources":["../src/remote-sync-handler.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Tests for RemoteSyncHandler
3
+ *
4
+ * Verifies remote → local sync with conflict detection and filesystem writes.
5
+ */
6
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
7
+ import { EventEmitter } from 'events';
8
+ // Mock fs at top level
9
+ vi.mock('fs', () => ({
10
+ default: {
11
+ existsSync: vi.fn(),
12
+ readFileSync: vi.fn(),
13
+ writeFileSync: vi.fn(),
14
+ mkdirSync: vi.fn()
15
+ }
16
+ }));
17
+ // Mock fetch at top level
18
+ global.fetch = vi.fn();
19
+ import { RemoteSyncHandler } from './remote-sync-handler.js';
20
+ import fs from 'fs';
21
+ describe('RemoteSyncHandler', () => {
22
+ let mockSubscriber;
23
+ let mockResolver;
24
+ let mockLogger;
25
+ let workspaces;
26
+ let handler;
27
+ beforeEach(() => {
28
+ // Mock RealtimeSubscriber
29
+ mockSubscriber = new EventEmitter();
30
+ mockSubscriber.detectConflict = vi.fn();
31
+ // Mock ConflictResolver
32
+ mockResolver = {
33
+ resolveConflict: vi.fn(),
34
+ commitConflict: vi.fn()
35
+ };
36
+ // Mock Logger
37
+ mockLogger = {
38
+ info: vi.fn(),
39
+ warn: vi.fn(),
40
+ error: vi.fn(),
41
+ debug: vi.fn()
42
+ };
43
+ // Setup workspaces
44
+ workspaces = new Map();
45
+ workspaces.set('ws-123', {
46
+ id: 'ws-123',
47
+ root_path: '/home/user/projects/my-project',
48
+ name: 'my-project',
49
+ status: 'active',
50
+ last_sync: '2026-03-27T00:00:00Z',
51
+ discovered_at: '2026-03-27T00:00:00Z'
52
+ });
53
+ handler = new RemoteSyncHandler(mockSubscriber, mockResolver, workspaces, mockLogger);
54
+ // Clear all mocks
55
+ vi.clearAllMocks();
56
+ });
57
+ afterEach(() => {
58
+ vi.clearAllMocks();
59
+ });
60
+ it('should write remote content to filesystem when handleRemoteChange is called', async () => {
61
+ const event = {
62
+ id: 'file-1',
63
+ workspace_id: 'ws-123',
64
+ file_path: '.planning/STATE.md',
65
+ content: 'Remote content',
66
+ content_hash: 'abc123',
67
+ size: 14,
68
+ updated_at: '2026-03-27T13:45:32Z'
69
+ };
70
+ vi.mocked(fs.existsSync).mockReturnValue(false);
71
+ vi.mocked(mockSubscriber.detectConflict).mockResolvedValue(null);
72
+ await handler.handleRemoteChange(event);
73
+ expect(fs.writeFileSync).toHaveBeenCalledWith('/home/user/projects/my-project/.planning/STATE.md', 'Remote content', 'utf8');
74
+ });
75
+ it('should detect conflict when local file differs', async () => {
76
+ const event = {
77
+ id: 'file-1',
78
+ workspace_id: 'ws-123',
79
+ file_path: '.planning/STATE.md',
80
+ content: 'Remote content',
81
+ content_hash: 'abc123',
82
+ size: 14,
83
+ updated_at: '2026-03-27T13:45:32Z'
84
+ };
85
+ vi.mocked(fs.existsSync).mockReturnValue(true);
86
+ vi.mocked(fs.readFileSync).mockReturnValue('Local content');
87
+ vi.mocked(mockSubscriber.detectConflict).mockResolvedValue({
88
+ workspace_id: 'ws-123',
89
+ file_path: '.planning/STATE.md',
90
+ local_hash: 'def456',
91
+ remote_hash: 'abc123',
92
+ local_content: 'Local content',
93
+ remote_content: 'Remote content',
94
+ detected_at: '2026-03-27T13:45:32Z'
95
+ });
96
+ await handler.handleRemoteChange(event);
97
+ expect(mockSubscriber.detectConflict).toHaveBeenCalledWith('ws-123', '.planning/STATE.md', 'abc123', 'Remote content', 'Local content');
98
+ });
99
+ it('should create conflict markers when conflict detected', async () => {
100
+ const event = {
101
+ id: 'file-1',
102
+ workspace_id: 'ws-123',
103
+ file_path: '.planning/STATE.md',
104
+ content: 'Remote content',
105
+ content_hash: 'abc123',
106
+ size: 14,
107
+ updated_at: '2026-03-27T13:45:32Z'
108
+ };
109
+ const conflict = {
110
+ workspace_id: 'ws-123',
111
+ file_path: '.planning/STATE.md',
112
+ local_hash: 'def456',
113
+ remote_hash: 'abc123',
114
+ local_content: 'Local content',
115
+ remote_content: 'Remote content',
116
+ detected_at: '2026-03-27T13:45:32Z'
117
+ };
118
+ vi.mocked(fs.existsSync).mockReturnValue(true);
119
+ vi.mocked(fs.readFileSync).mockReturnValue('Local content');
120
+ vi.mocked(mockSubscriber.detectConflict).mockResolvedValue(conflict);
121
+ vi.mocked(mockResolver.resolveConflict).mockReturnValue('<<<<<<< LOCAL\nLocal content\n=======\nRemote content\n>>>>>>> REMOTE');
122
+ await handler.handleRemoteChange(event);
123
+ expect(mockResolver.resolveConflict).toHaveBeenCalledWith(conflict);
124
+ expect(fs.writeFileSync).toHaveBeenCalledWith('/home/user/projects/my-project/.planning/STATE.md', expect.stringContaining('<<<<<<< LOCAL'), 'utf8');
125
+ });
126
+ it('should commit conflicted file to git', async () => {
127
+ const event = {
128
+ id: 'file-1',
129
+ workspace_id: 'ws-123',
130
+ file_path: '.planning/STATE.md',
131
+ content: 'Remote content',
132
+ content_hash: 'abc123',
133
+ size: 14,
134
+ updated_at: '2026-03-27T13:45:32Z'
135
+ };
136
+ const conflict = {
137
+ workspace_id: 'ws-123',
138
+ file_path: '.planning/STATE.md',
139
+ local_hash: 'def456',
140
+ remote_hash: 'abc123',
141
+ local_content: 'Local content',
142
+ remote_content: 'Remote content',
143
+ detected_at: '2026-03-27T13:45:32Z'
144
+ };
145
+ vi.mocked(fs.existsSync).mockReturnValue(true);
146
+ vi.mocked(fs.readFileSync).mockReturnValue('Local content');
147
+ vi.mocked(mockSubscriber.detectConflict).mockResolvedValue(conflict);
148
+ vi.mocked(mockResolver.resolveConflict).mockReturnValue('conflict markers');
149
+ await handler.handleRemoteChange(event);
150
+ expect(mockResolver.commitConflict).toHaveBeenCalledWith('/home/user/projects/my-project', '.planning/STATE.md');
151
+ });
152
+ it('should skip write during local sync to prevent loops', async () => {
153
+ const event = {
154
+ id: 'file-1',
155
+ workspace_id: 'ws-123',
156
+ file_path: '.planning/STATE.md',
157
+ content: 'Remote content',
158
+ content_hash: 'abc123',
159
+ size: 14,
160
+ updated_at: '2026-03-27T13:45:32Z'
161
+ };
162
+ vi.mocked(fs.existsSync).mockReturnValue(false);
163
+ // Create a promise that we control
164
+ let resolveWrite;
165
+ const writeDelay = new Promise(resolve => { resolveWrite = resolve; });
166
+ vi.mocked(fs.writeFileSync).mockImplementationOnce(() => {
167
+ // Don't actually block, just track the call
168
+ });
169
+ // Manually add to syncing set to simulate in-progress sync
170
+ const syncKey = `${event.workspace_id}:${event.file_path}`;
171
+ handler.syncingFiles.add(syncKey);
172
+ // Try to sync while already in progress
173
+ await handler.handleRemoteChange(event);
174
+ // Should log that sync was skipped
175
+ expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining('Skipping remote change - sync already in progress'), expect.any(Object));
176
+ // Should not write file
177
+ expect(fs.writeFileSync).not.toHaveBeenCalled();
178
+ handler.syncingFiles.delete(syncKey);
179
+ });
180
+ it('should subscribe to RealtimeSubscriber remote-change events on start', () => {
181
+ handler.start();
182
+ expect(mockSubscriber.listenerCount('remote-change')).toBeGreaterThan(0);
183
+ expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Remote sync handler started'), expect.any(Object));
184
+ });
185
+ it('should unsubscribe from all events on stop', () => {
186
+ handler.start();
187
+ handler.stop();
188
+ expect(mockSubscriber.listenerCount('remote-change')).toBe(0);
189
+ expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Remote sync handler stopped'), expect.any(Object));
190
+ });
191
+ it('should download large files from storage_url', async () => {
192
+ const event = {
193
+ id: 'file-1',
194
+ workspace_id: 'ws-123',
195
+ file_path: '.planning/large-file.md',
196
+ content: null,
197
+ content_hash: 'abc123',
198
+ size: 300000,
199
+ updated_at: '2026-03-27T13:45:32Z',
200
+ storage_url: 'https://storage.supabase.co/bucket/file.md'
201
+ };
202
+ vi.mocked(fs.existsSync).mockReturnValue(false);
203
+ vi.mocked(global.fetch).mockResolvedValue({
204
+ ok: true,
205
+ text: async () => 'Large file content'
206
+ });
207
+ await handler.handleRemoteChange(event);
208
+ expect(global.fetch).toHaveBeenCalledWith('https://storage.supabase.co/bucket/file.md');
209
+ expect(fs.writeFileSync).toHaveBeenCalledWith('/home/user/projects/my-project/.planning/large-file.md', 'Large file content', 'utf8');
210
+ });
211
+ });
212
+ //# sourceMappingURL=remote-sync-handler.test.js.map