pushwork 1.0.4 → 1.0.7

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 (195) hide show
  1. package/README.md +87 -328
  2. package/dist/.pushwork/automerge/3P/Dm3ekE2pmjGnWvDaG3vSR7ww98/snapshot/aa2349c94955ea561f698720142f9d884a6872d9f82dc332d578c216beb0df0e +0 -0
  3. package/dist/.pushwork/automerge/st/orage-adapter-id +1 -0
  4. package/dist/.pushwork/config.json +15 -0
  5. package/dist/.pushwork/snapshot.json +7 -0
  6. package/dist/cli.js +231 -170
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands.d.ts +51 -0
  9. package/dist/commands.d.ts.map +1 -0
  10. package/dist/commands.js +799 -0
  11. package/dist/commands.js.map +1 -0
  12. package/dist/core/change-detection.d.ts +6 -19
  13. package/dist/core/change-detection.d.ts.map +1 -1
  14. package/dist/core/change-detection.js +101 -80
  15. package/dist/core/change-detection.js.map +1 -1
  16. package/dist/{config/index.d.ts → core/config.d.ts} +13 -3
  17. package/dist/core/config.d.ts.map +1 -0
  18. package/dist/{config/index.js → core/config.js} +55 -73
  19. package/dist/core/config.js.map +1 -0
  20. package/dist/core/index.d.ts +1 -0
  21. package/dist/core/index.d.ts.map +1 -1
  22. package/dist/core/index.js +1 -1
  23. package/dist/core/index.js.map +1 -1
  24. package/dist/core/move-detection.d.ts +12 -50
  25. package/dist/core/move-detection.d.ts.map +1 -1
  26. package/dist/core/move-detection.js +58 -139
  27. package/dist/core/move-detection.js.map +1 -1
  28. package/dist/core/snapshot.d.ts +0 -4
  29. package/dist/core/snapshot.d.ts.map +1 -1
  30. package/dist/core/snapshot.js +2 -11
  31. package/dist/core/snapshot.js.map +1 -1
  32. package/dist/core/sync-engine.d.ts +5 -11
  33. package/dist/core/sync-engine.d.ts.map +1 -1
  34. package/dist/core/sync-engine.js +220 -362
  35. package/dist/core/sync-engine.js.map +1 -1
  36. package/dist/index.d.ts +0 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +0 -6
  39. package/dist/index.js.map +1 -1
  40. package/dist/types/config.d.ts +43 -67
  41. package/dist/types/config.d.ts.map +1 -1
  42. package/dist/types/config.js +6 -0
  43. package/dist/types/config.js.map +1 -1
  44. package/dist/types/documents.d.ts +15 -3
  45. package/dist/types/documents.d.ts.map +1 -1
  46. package/dist/types/documents.js.map +1 -1
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/types/index.js +0 -3
  49. package/dist/types/index.js.map +1 -1
  50. package/dist/types/snapshot.d.ts +3 -21
  51. package/dist/types/snapshot.d.ts.map +1 -1
  52. package/dist/types/snapshot.js +0 -14
  53. package/dist/types/snapshot.js.map +1 -1
  54. package/dist/utils/content.d.ts.map +1 -1
  55. package/dist/utils/content.js +2 -6
  56. package/dist/utils/content.js.map +1 -1
  57. package/dist/utils/directory.d.ts +10 -0
  58. package/dist/utils/directory.d.ts.map +1 -0
  59. package/dist/utils/directory.js +37 -0
  60. package/dist/utils/directory.js.map +1 -0
  61. package/dist/utils/fs.d.ts +15 -2
  62. package/dist/utils/fs.d.ts.map +1 -1
  63. package/dist/utils/fs.js +63 -53
  64. package/dist/utils/fs.js.map +1 -1
  65. package/dist/utils/index.d.ts +1 -1
  66. package/dist/utils/index.d.ts.map +1 -1
  67. package/dist/utils/index.js +1 -4
  68. package/dist/utils/index.js.map +1 -1
  69. package/dist/utils/mime-types.d.ts.map +1 -1
  70. package/dist/utils/mime-types.js +11 -4
  71. package/dist/utils/mime-types.js.map +1 -1
  72. package/dist/utils/network-sync.d.ts +0 -6
  73. package/dist/utils/network-sync.d.ts.map +1 -1
  74. package/dist/utils/network-sync.js +55 -99
  75. package/dist/utils/network-sync.js.map +1 -1
  76. package/dist/utils/output.d.ts +129 -0
  77. package/dist/utils/output.d.ts.map +1 -0
  78. package/dist/utils/output.js +375 -0
  79. package/dist/utils/output.js.map +1 -0
  80. package/dist/utils/repo-factory.d.ts +2 -6
  81. package/dist/utils/repo-factory.d.ts.map +1 -1
  82. package/dist/utils/repo-factory.js +8 -22
  83. package/dist/utils/repo-factory.js.map +1 -1
  84. package/dist/utils/string-similarity.d.ts +14 -0
  85. package/dist/utils/string-similarity.d.ts.map +1 -0
  86. package/dist/utils/string-similarity.js +43 -0
  87. package/dist/utils/string-similarity.js.map +1 -0
  88. package/dist/utils/trace.d.ts +19 -0
  89. package/dist/utils/trace.d.ts.map +1 -0
  90. package/dist/utils/trace.js +68 -0
  91. package/dist/utils/trace.js.map +1 -0
  92. package/package.json +17 -12
  93. package/src/cli.ts +326 -252
  94. package/src/commands.ts +988 -0
  95. package/src/core/change-detection.ts +199 -162
  96. package/src/{config/index.ts → core/config.ts} +65 -82
  97. package/src/core/index.ts +1 -1
  98. package/src/core/move-detection.ts +74 -180
  99. package/src/core/snapshot.ts +2 -12
  100. package/src/core/sync-engine.ts +248 -499
  101. package/src/index.ts +0 -10
  102. package/src/types/config.ts +50 -72
  103. package/src/types/documents.ts +16 -3
  104. package/src/types/index.ts +0 -5
  105. package/src/types/snapshot.ts +1 -23
  106. package/src/utils/content.ts +2 -6
  107. package/src/utils/directory.ts +50 -0
  108. package/src/utils/fs.ts +67 -56
  109. package/src/utils/index.ts +1 -6
  110. package/src/utils/mime-types.ts +12 -4
  111. package/src/utils/network-sync.ts +79 -137
  112. package/src/utils/output.ts +450 -0
  113. package/src/utils/repo-factory.ts +13 -31
  114. package/src/utils/string-similarity.ts +54 -0
  115. package/src/utils/trace.ts +70 -0
  116. package/test/integration/exclude-patterns.test.ts +6 -15
  117. package/test/integration/fuzzer.test.ts +308 -391
  118. package/test/integration/init-sync.test.ts +89 -0
  119. package/test/integration/sync-deletion.test.ts +2 -61
  120. package/test/integration/sync-flow.test.ts +4 -24
  121. package/test/jest.setup.ts +34 -0
  122. package/test/unit/deletion-behavior.test.ts +3 -14
  123. package/test/unit/enhanced-mime-detection.test.ts +0 -22
  124. package/test/unit/snapshot.test.ts +2 -29
  125. package/test/unit/sync-convergence.test.ts +3 -198
  126. package/test/unit/sync-timing.test.ts +0 -44
  127. package/test/unit/utils.test.ts +0 -2
  128. package/tsconfig.json +3 -3
  129. package/dist/browser/browser-sync-engine.d.ts +0 -64
  130. package/dist/browser/browser-sync-engine.d.ts.map +0 -1
  131. package/dist/browser/browser-sync-engine.js +0 -303
  132. package/dist/browser/browser-sync-engine.js.map +0 -1
  133. package/dist/browser/filesystem-adapter.d.ts +0 -84
  134. package/dist/browser/filesystem-adapter.d.ts.map +0 -1
  135. package/dist/browser/filesystem-adapter.js +0 -413
  136. package/dist/browser/filesystem-adapter.js.map +0 -1
  137. package/dist/browser/index.d.ts +0 -36
  138. package/dist/browser/index.d.ts.map +0 -1
  139. package/dist/browser/index.js +0 -90
  140. package/dist/browser/index.js.map +0 -1
  141. package/dist/browser/types.d.ts +0 -70
  142. package/dist/browser/types.d.ts.map +0 -1
  143. package/dist/browser/types.js +0 -6
  144. package/dist/browser/types.js.map +0 -1
  145. package/dist/cli/commands.d.ts +0 -77
  146. package/dist/cli/commands.d.ts.map +0 -1
  147. package/dist/cli/commands.js +0 -904
  148. package/dist/cli/commands.js.map +0 -1
  149. package/dist/cli/index.d.ts +0 -2
  150. package/dist/cli/index.d.ts.map +0 -1
  151. package/dist/cli/index.js +0 -19
  152. package/dist/cli/index.js.map +0 -1
  153. package/dist/config/index.d.ts.map +0 -1
  154. package/dist/config/index.js.map +0 -1
  155. package/dist/core/isomorphic-snapshot.d.ts +0 -58
  156. package/dist/core/isomorphic-snapshot.d.ts.map +0 -1
  157. package/dist/core/isomorphic-snapshot.js +0 -204
  158. package/dist/core/isomorphic-snapshot.js.map +0 -1
  159. package/dist/platform/browser-filesystem.d.ts +0 -26
  160. package/dist/platform/browser-filesystem.d.ts.map +0 -1
  161. package/dist/platform/browser-filesystem.js +0 -91
  162. package/dist/platform/browser-filesystem.js.map +0 -1
  163. package/dist/platform/filesystem.d.ts +0 -29
  164. package/dist/platform/filesystem.d.ts.map +0 -1
  165. package/dist/platform/filesystem.js +0 -65
  166. package/dist/platform/filesystem.js.map +0 -1
  167. package/dist/platform/node-filesystem.d.ts +0 -21
  168. package/dist/platform/node-filesystem.d.ts.map +0 -1
  169. package/dist/platform/node-filesystem.js +0 -93
  170. package/dist/platform/node-filesystem.js.map +0 -1
  171. package/dist/utils/content-similarity.d.ts +0 -53
  172. package/dist/utils/content-similarity.d.ts.map +0 -1
  173. package/dist/utils/content-similarity.js +0 -155
  174. package/dist/utils/content-similarity.js.map +0 -1
  175. package/dist/utils/fs-browser.d.ts +0 -57
  176. package/dist/utils/fs-browser.d.ts.map +0 -1
  177. package/dist/utils/fs-browser.js +0 -311
  178. package/dist/utils/fs-browser.js.map +0 -1
  179. package/dist/utils/fs-node.d.ts +0 -53
  180. package/dist/utils/fs-node.d.ts.map +0 -1
  181. package/dist/utils/fs-node.js +0 -220
  182. package/dist/utils/fs-node.js.map +0 -1
  183. package/dist/utils/isomorphic.d.ts +0 -29
  184. package/dist/utils/isomorphic.d.ts.map +0 -1
  185. package/dist/utils/isomorphic.js +0 -139
  186. package/dist/utils/isomorphic.js.map +0 -1
  187. package/dist/utils/pure.d.ts +0 -25
  188. package/dist/utils/pure.d.ts.map +0 -1
  189. package/dist/utils/pure.js +0 -112
  190. package/dist/utils/pure.js.map +0 -1
  191. package/src/cli/commands.ts +0 -1207
  192. package/src/cli/index.ts +0 -2
  193. package/src/utils/content-similarity.ts +0 -194
  194. package/test/README-TESTING-GAPS.md +0 -174
  195. package/test/unit/content-similarity.test.ts +0 -236
@@ -119,47 +119,32 @@ describe("Pushwork Fuzzer", () => {
119
119
  await fs.mkdir(repoA);
120
120
  await fs.mkdir(repoB);
121
121
 
122
- console.log(`Test directories created:`);
123
- console.log(` Repo A: ${repoA}`);
124
- console.log(` Repo B: ${repoB}`);
125
-
126
122
  // Step 1: Create a file in repo A
127
123
  const testFile = path.join(repoA, "test.txt");
128
124
  await fs.writeFile(testFile, "Hello, Pushwork!");
129
- console.log(`Created test file: ${testFile}`);
130
125
 
131
126
  // Step 2: Initialize repo A
132
- console.log(`Initializing repo A...`);
133
127
  await pushwork(["init", "."], repoA);
134
- console.log(`Repo A initialized successfully`);
135
128
 
136
129
  // Wait a moment for initialization to complete
137
130
  await wait(1000);
138
131
 
139
132
  // Step 3: Get the root URL from repo A
140
- console.log(`Getting root URL from repo A...`);
141
133
  const { stdout: rootUrl } = await pushwork(["url"], repoA);
142
134
  const cleanRootUrl = rootUrl.trim();
143
- console.log(`Root URL: ${cleanRootUrl}`);
144
135
 
145
136
  expect(cleanRootUrl).toMatch(/^automerge:/);
146
137
 
147
138
  // Step 4: Clone repo A to repo B
148
- console.log(`Cloning repo A to repo B...`);
149
139
  await pushwork(["clone", cleanRootUrl, repoB], tmpDir);
150
- console.log(`Repo B cloned successfully`);
151
140
 
152
141
  // Wait a moment for clone to complete
153
142
  await wait(1000);
154
143
 
155
144
  // Step 5: Verify both repos have the same content
156
- console.log(`Computing hashes...`);
157
145
  const hashA = await hashDirectory(repoA);
158
146
  const hashB = await hashDirectory(repoB);
159
147
 
160
- console.log(`Hash A: ${hashA}`);
161
- console.log(`Hash B: ${hashB}`);
162
-
163
148
  expect(hashA).toBe(hashB);
164
149
 
165
150
  // Step 6: Verify the file exists in both repos
@@ -176,325 +161,333 @@ describe("Pushwork Fuzzer", () => {
176
161
  expect(contentA).toBe("Hello, Pushwork!");
177
162
  expect(contentB).toBe("Hello, Pushwork!");
178
163
  expect(contentA).toBe(contentB);
179
-
180
- console.log(`✅ Test passed! Both repos are identical.`);
181
- }, 30000); // 30 second timeout for this test
164
+ }, 10000); // 10 second timeout for this test
182
165
  });
183
166
 
184
167
  describe("Manual Fuzzing Tests", () => {
185
- it("should handle a simple edit on one side", async () => {
186
- const repoA = path.join(tmpDir, "manual-a");
187
- const repoB = path.join(tmpDir, "manual-b");
188
- await fs.mkdir(repoA);
189
- await fs.mkdir(repoB);
190
-
191
- // Initialize repo A with a file
192
- await fs.writeFile(path.join(repoA, "test.txt"), "initial content");
193
- await pushwork(["init", "."], repoA);
194
- await wait(500);
195
-
196
- // Clone to B
197
- const { stdout: rootUrl } = await pushwork(["url"], repoA);
198
- await pushwork(["clone", rootUrl.trim(), repoB], tmpDir);
199
- await wait(500);
200
-
201
- // Edit file on A
202
- await fs.writeFile(path.join(repoA, "test.txt"), "modified content");
203
-
204
- // Sync A
205
- await pushwork(["sync"], repoA);
206
- await wait(1000);
207
-
208
- // Sync B to pull changes
209
- await pushwork(["sync"], repoB);
210
- await wait(1000);
211
-
212
- // Verify they match
213
- const contentA = await fs.readFile(path.join(repoA, "test.txt"), "utf-8");
214
- const contentB = await fs.readFile(path.join(repoB, "test.txt"), "utf-8");
215
-
216
- expect(contentA).toBe("modified content");
217
- expect(contentB).toBe("modified content");
218
- }, 30000);
219
-
220
- it("should handle edit + rename on one side", async () => {
221
- const repoA = path.join(tmpDir, "rename-a");
222
- const repoB = path.join(tmpDir, "rename-b");
223
- await fs.mkdir(repoA);
224
- await fs.mkdir(repoB);
225
-
226
- // Initialize repo A with a file
227
- await fs.writeFile(path.join(repoA, "original.txt"), "original content");
228
- await pushwork(["init", "."], repoA);
229
- await wait(500);
230
-
231
- // Clone to B
232
- const { stdout: rootUrl } = await pushwork(["url"], repoA);
233
- await pushwork(["clone", rootUrl.trim(), repoB], tmpDir);
234
- await wait(500);
235
-
236
- // Edit AND rename file on A (the suspicious operation!)
237
- await fs.writeFile(path.join(repoA, "original.txt"), "edited content");
238
- await fs.rename(
239
- path.join(repoA, "original.txt"),
240
- path.join(repoA, "renamed.txt")
241
- );
168
+ it.concurrent(
169
+ "should handle a simple edit on one side",
170
+ async () => {
171
+ const tmpObj = tmp.dirSync({ unsafeCleanup: true });
172
+ const testRoot = path.join(
173
+ tmpObj.name,
174
+ `test-manual-a-${Date.now()}-${Math.random()}`
175
+ );
176
+ await fs.mkdir(testRoot, { recursive: true });
177
+ const repoA = path.join(testRoot, "manual-a");
178
+ const repoB = path.join(testRoot, "manual-b");
179
+ await fs.mkdir(repoA);
180
+ await fs.mkdir(repoB);
181
+
182
+ // Initialize repo A with a file
183
+ await fs.writeFile(path.join(repoA, "test.txt"), "initial content");
184
+ await pushwork(["init", "."], repoA);
185
+ await wait(500);
186
+
187
+ // Clone to B
188
+ const { stdout: rootUrl } = await pushwork(["url"], repoA);
189
+ await pushwork(["clone", rootUrl.trim(), repoB], tmpDir);
190
+ await wait(500);
191
+
192
+ // Edit file on A
193
+ await fs.writeFile(path.join(repoA, "test.txt"), "modified content");
194
+
195
+ // Sync A
196
+ await pushwork(["sync"], repoA);
197
+ await wait(1000);
198
+
199
+ // Sync B to pull changes
200
+ await pushwork(["sync"], repoB);
201
+ await wait(1000);
202
+
203
+ // Verify they match
204
+ const contentA = await fs.readFile(
205
+ path.join(repoA, "test.txt"),
206
+ "utf-8"
207
+ );
208
+ const contentB = await fs.readFile(
209
+ path.join(repoB, "test.txt"),
210
+ "utf-8"
211
+ );
242
212
 
243
- // Sync both sides
244
- await pushwork(["sync"], repoA);
245
- await wait(1000);
246
- await pushwork(["sync"], repoB);
247
- await wait(1000);
213
+ expect(contentA).toBe("modified content");
214
+ expect(contentB).toBe("modified content");
248
215
 
249
- // One more round for convergence
250
- await pushwork(["sync"], repoA);
251
- await wait(1000);
252
- await pushwork(["sync"], repoB);
253
- await wait(1000);
216
+ // Cleanup
217
+ tmpObj.removeCallback();
218
+ },
219
+ 30000
220
+ );
254
221
 
255
- // Verify: original.txt should not exist, renamed.txt should exist with edited content
256
- const originalExistsA = await pathExists(
257
- path.join(repoA, "original.txt")
258
- );
259
- const originalExistsB = await pathExists(
260
- path.join(repoB, "original.txt")
261
- );
262
- const renamedExistsA = await pathExists(path.join(repoA, "renamed.txt"));
263
- const renamedExistsB = await pathExists(path.join(repoB, "renamed.txt"));
222
+ it.concurrent(
223
+ "should handle edit + rename on one side",
224
+ async () => {
225
+ const tmpObj = tmp.dirSync({ unsafeCleanup: true });
226
+ const testRoot = path.join(
227
+ tmpObj.name,
228
+ `test-rename-${Date.now()}-${Math.random()}`
229
+ );
230
+ await fs.mkdir(testRoot, { recursive: true });
231
+ const repoA = path.join(testRoot, "rename-a");
232
+ const repoB = path.join(testRoot, "rename-b");
233
+ await fs.mkdir(repoA);
234
+ await fs.mkdir(repoB);
235
+
236
+ // Initialize repo A with a file
237
+ await fs.writeFile(
238
+ path.join(repoA, "original.txt"),
239
+ "original content"
240
+ );
241
+ await pushwork(["init", "."], repoA);
242
+ await wait(500);
243
+
244
+ // Clone to B
245
+ const { stdout: rootUrl } = await pushwork(["url"], repoA);
246
+ await pushwork(["clone", rootUrl.trim(), repoB], tmpDir);
247
+ await wait(500);
248
+
249
+ // Edit AND rename file on A (the suspicious operation!)
250
+ await fs.writeFile(path.join(repoA, "original.txt"), "edited content");
251
+ await fs.rename(
252
+ path.join(repoA, "original.txt"),
253
+ path.join(repoA, "renamed.txt")
254
+ );
264
255
 
265
- expect(originalExistsA).toBe(false);
266
- expect(originalExistsB).toBe(false);
267
- expect(renamedExistsA).toBe(true);
268
- expect(renamedExistsB).toBe(true);
256
+ // Sync both sides
257
+ await pushwork(["sync"], repoA);
258
+ await wait(1000);
259
+ await pushwork(["sync"], repoB);
260
+ await wait(1000);
261
+
262
+ // One more round for convergence
263
+ await pushwork(["sync"], repoA);
264
+ await wait(1000);
265
+ await pushwork(["sync"], repoB);
266
+ await wait(1000);
267
+
268
+ // Verify: original.txt should not exist, renamed.txt should exist with edited content
269
+ const originalExistsA = await pathExists(
270
+ path.join(repoA, "original.txt")
271
+ );
272
+ const originalExistsB = await pathExists(
273
+ path.join(repoB, "original.txt")
274
+ );
275
+ const renamedExistsA = await pathExists(
276
+ path.join(repoA, "renamed.txt")
277
+ );
278
+ const renamedExistsB = await pathExists(
279
+ path.join(repoB, "renamed.txt")
280
+ );
269
281
 
270
- const contentA = await fs.readFile(
271
- path.join(repoA, "renamed.txt"),
272
- "utf-8"
273
- );
274
- const contentB = await fs.readFile(
275
- path.join(repoB, "renamed.txt"),
276
- "utf-8"
277
- );
282
+ expect(originalExistsA).toBe(false);
283
+ expect(originalExistsB).toBe(false);
284
+ expect(renamedExistsA).toBe(true);
285
+ expect(renamedExistsB).toBe(true);
278
286
 
279
- expect(contentA).toBe("edited content");
280
- expect(contentB).toBe("edited content");
281
- }, 120000); // 2 minute timeout
287
+ const contentA = await fs.readFile(
288
+ path.join(repoA, "renamed.txt"),
289
+ "utf-8"
290
+ );
291
+ const contentB = await fs.readFile(
292
+ path.join(repoB, "renamed.txt"),
293
+ "utf-8"
294
+ );
282
295
 
283
- it("should handle simplest case: clone then add file", async () => {
284
- const repoA = path.join(tmpDir, "simple-a");
285
- const repoB = path.join(tmpDir, "simple-b");
286
- await fs.mkdir(repoA);
287
- await fs.mkdir(repoB);
296
+ expect(contentA).toBe("edited content");
297
+ expect(contentB).toBe("edited content");
298
+
299
+ // Cleanup
300
+ tmpObj.removeCallback();
301
+ },
302
+ 120000
303
+ ); // 2 minute timeout
304
+
305
+ it.concurrent(
306
+ "should handle simplest case: clone then add file",
307
+ async () => {
308
+ const tmpObj = tmp.dirSync({ unsafeCleanup: true });
309
+ const testRoot = path.join(
310
+ tmpObj.name,
311
+ `test-simple-${Date.now()}-${Math.random()}`
312
+ );
313
+ await fs.mkdir(testRoot, { recursive: true });
314
+ const repoA = path.join(testRoot, "simple-a");
315
+ const repoB = path.join(testRoot, "simple-b");
316
+ await fs.mkdir(repoA);
317
+ await fs.mkdir(repoB);
318
+
319
+ // Initialize repo A
320
+ await fs.writeFile(path.join(repoA, "initial.txt"), "initial");
321
+ await pushwork(["init", "."], repoA);
322
+ await wait(1000);
323
+
324
+ // Clone to B
325
+ const { stdout: rootUrl } = await pushwork(["url"], repoA);
326
+ await pushwork(["clone", rootUrl.trim(), repoB], tmpDir);
327
+ await wait(1000);
328
+
329
+ // B: Create a new file (nothing else happens)
330
+ await fs.writeFile(path.join(repoB, "aaa.txt"), "");
331
+
332
+ // B syncs
333
+ await pushwork(["sync"], repoB);
334
+ await wait(1000);
335
+
336
+ // A syncs
337
+ await pushwork(["sync"], repoA);
338
+ await wait(1000);
339
+
340
+ // Check convergence
341
+ const filesA = await fs.readdir(repoA);
342
+ const filesB = await fs.readdir(repoB);
343
+ const filteredFilesA = filesA.filter((f) => !f.startsWith("."));
344
+ const filteredFilesB = filesB.filter((f) => !f.startsWith("."));
345
+ expect(filteredFilesA).toEqual(filteredFilesB);
346
+
347
+ expect(await pathExists(path.join(repoA, "aaa.txt"))).toBe(true);
348
+ expect(await pathExists(path.join(repoB, "aaa.txt"))).toBe(true);
349
+
350
+ // Cleanup
351
+ tmpObj.removeCallback();
352
+ },
353
+ 20000
354
+ );
288
355
 
289
- // Initialize repo A
290
- await fs.writeFile(path.join(repoA, "initial.txt"), "initial");
291
- await pushwork(["init", "."], repoA);
292
- await wait(1000);
356
+ it.concurrent(
357
+ "should handle minimal shrunk case: editAndRename non-existent + add same file",
358
+ async () => {
359
+ const tmpObj = tmp.dirSync({ unsafeCleanup: true });
360
+ const testRoot = path.join(
361
+ tmpObj.name,
362
+ `test-shrunk-${Date.now()}-${Math.random()}`
363
+ );
364
+ await fs.mkdir(testRoot, { recursive: true });
365
+ const repoA = path.join(testRoot, "shrunk-a");
366
+ const repoB = path.join(testRoot, "shrunk-b");
367
+ await fs.mkdir(repoA);
368
+ await fs.mkdir(repoB);
369
+
370
+ // Initialize repo A
371
+ await fs.writeFile(path.join(repoA, "initial.txt"), "initial");
372
+ await pushwork(["init", "."], repoA);
373
+ await wait(1000); // Match manual test timing
374
+
375
+ // Clone to B
376
+ const { stdout: rootUrl } = await pushwork(["url"], repoA);
377
+ await pushwork(["clone", rootUrl.trim(), repoB], tmpDir);
378
+ await wait(1000); // Match manual test timing
379
+
380
+ // A: Try to editAndRename a non-existent file (this is from the shrunk test case)
381
+ // This operation should be a no-op since aaa.txt doesn't exist
382
+ const fromPath = path.join(repoA, "aaa.txt");
383
+ const toPath = path.join(repoA, "aa/aa/aaa.txt");
384
+ if ((await pathExists(fromPath)) && !(await pathExists(toPath))) {
385
+ await fs.writeFile(fromPath, "");
386
+ await fs.mkdir(path.dirname(toPath), { recursive: true });
387
+ await fs.rename(fromPath, toPath);
388
+ }
293
389
 
294
- // Clone to B
295
- const { stdout: rootUrl } = await pushwork(["url"], repoA);
296
- await pushwork(["clone", rootUrl.trim(), repoB], tmpDir);
297
- await wait(1000);
390
+ // B: Create the same file that A tried to operate on
391
+ await fs.writeFile(path.join(repoB, "aaa.txt"), "");
298
392
 
299
- // B: Create a new file (nothing else happens)
300
- await fs.writeFile(path.join(repoB, "aaa.txt"), "");
301
- console.log("Created aaa.txt in B");
393
+ // Sync multiple rounds (use 1s waits for reliable network propagation)
394
+ // Pattern: A, B, A (like manual test that worked)
395
+ await pushwork(["sync"], repoA);
396
+ await wait(1000);
302
397
 
303
- // B syncs
304
- console.log("B sync...");
305
- const syncB = await pushwork(["sync"], repoB);
306
- console.log("B pushed aaa.txt?", syncB.stdout.includes("aaa.txt"));
307
- console.log("B full output:\n", syncB.stdout);
308
- await wait(1000);
398
+ // Check what B sees before sync
399
+ await pushwork(["diff", "--name-only"], repoB);
309
400
 
310
- // A syncs
311
- console.log("A sync...");
312
- const syncA = await pushwork(["sync"], repoA);
313
- console.log("A pulled aaa.txt?", syncA.stdout.includes("aaa.txt"));
314
- await wait(1000);
401
+ await pushwork(["sync"], repoB);
402
+ await wait(1000);
315
403
 
316
- // Check convergence
317
- const filesA = await fs.readdir(repoA);
318
- const filesB = await fs.readdir(repoB);
319
- console.log(
320
- "Files in A:",
321
- filesA.filter((f) => !f.startsWith("."))
322
- );
323
- console.log(
324
- "Files in B:",
325
- filesB.filter((f) => !f.startsWith("."))
326
- );
404
+ await pushwork(["sync"], repoA);
405
+ await wait(1000);
327
406
 
328
- expect(await pathExists(path.join(repoA, "aaa.txt"))).toBe(true);
329
- expect(await pathExists(path.join(repoB, "aaa.txt"))).toBe(true);
330
- }, 30000);
407
+ // Debug: Check what files exist
408
+ const filesA = await fs.readdir(repoA);
409
+ const filesB = await fs.readdir(repoB);
410
+ const filteredFilesA = filesA.filter((f) => !f.startsWith("."));
411
+ const filteredFilesB = filesB.filter((f) => !f.startsWith("."));
412
+ expect(filteredFilesA).toEqual(filteredFilesB);
331
413
 
332
- it("should handle minimal shrunk case: editAndRename non-existent + add same file", async () => {
333
- const repoA = path.join(tmpDir, "shrunk-a");
334
- const repoB = path.join(tmpDir, "shrunk-b");
335
- await fs.mkdir(repoA);
336
- await fs.mkdir(repoB);
414
+ // Verify convergence
415
+ const hashA = await hashDirectory(repoA);
416
+ const hashB = await hashDirectory(repoB);
337
417
 
338
- // Initialize repo A
339
- await fs.writeFile(path.join(repoA, "initial.txt"), "initial");
340
- await pushwork(["init", "."], repoA);
341
- await wait(1000); // Match manual test timing
418
+ expect(hashA).toBe(hashB);
342
419
 
343
- // Clone to B
344
- const { stdout: rootUrl } = await pushwork(["url"], repoA);
345
- await pushwork(["clone", rootUrl.trim(), repoB], tmpDir);
346
- await wait(1000); // Match manual test timing
347
-
348
- // A: Try to editAndRename a non-existent file (this is from the shrunk test case)
349
- // This operation should be a no-op since aaa.txt doesn't exist
350
- const fromPath = path.join(repoA, "aaa.txt");
351
- const toPath = path.join(repoA, "aa/aa/aaa.txt");
352
- if ((await pathExists(fromPath)) && !(await pathExists(toPath))) {
353
- await fs.writeFile(fromPath, "");
354
- await fs.mkdir(path.dirname(toPath), { recursive: true });
355
- await fs.rename(fromPath, toPath);
356
- console.log("Applied editAndRename to A");
357
- } else {
358
- console.log("Skipped editAndRename to A (file doesn't exist)");
359
- }
420
+ // Both should have aaa.txt
421
+ expect(await pathExists(path.join(repoA, "aaa.txt"))).toBe(true);
422
+ expect(await pathExists(path.join(repoB, "aaa.txt"))).toBe(true);
360
423
 
361
- // B: Create the same file that A tried to operate on
362
- await fs.writeFile(path.join(repoB, "aaa.txt"), "");
363
- console.log("Created aaa.txt in B");
364
-
365
- // Sync multiple rounds (use 1s waits for reliable network propagation)
366
- // Pattern: A, B, A (like manual test that worked)
367
- console.log("Round 1: A sync...");
368
- const sync1 = await pushwork(["sync"], repoA);
369
- console.log(
370
- " A result:",
371
- sync1.stdout.includes("already in sync") ? "no changes" : "had changes"
372
- );
373
- await wait(1000);
374
-
375
- console.log("Round 2: B sync (should push aaa.txt)...");
376
-
377
- // Check what B sees before sync
378
- const bDiffBefore = await pushwork(["diff", "--name-only"], repoB);
379
- console.log(
380
- " B diff before sync:",
381
- bDiffBefore.stdout
382
- .split("\n")
383
- .filter((l) => !l.includes("✓") && l.trim())
384
- );
424
+ // Cleanup
425
+ tmpObj.removeCallback();
426
+ },
427
+ 20000
428
+ );
385
429
 
386
- // Check B's snapshot
387
- const bSnapshotPath = path.join(repoB, ".pushwork", "snapshot.json");
388
- if (await pathExists(bSnapshotPath)) {
389
- const bSnapshot = JSON.parse(await fs.readFile(bSnapshotPath, "utf8"));
390
- console.log(
391
- " B snapshot files:",
392
- Array.from(Object.keys(bSnapshot.files || {}))
430
+ it.concurrent(
431
+ "should handle files in subdirectories and moves between directories",
432
+ async () => {
433
+ const tmpObj = tmp.dirSync({ unsafeCleanup: true });
434
+ const testRoot = path.join(
435
+ tmpObj.name,
436
+ `test-subdir-${Date.now()}-${Math.random()}`
393
437
  );
394
- console.log(
395
- " B snapshot has aaa.txt?",
396
- bSnapshot.files && bSnapshot.files["aaa.txt"] ? "YES" : "NO"
438
+ await fs.mkdir(testRoot, { recursive: true });
439
+ const repoA = path.join(testRoot, "subdir-a");
440
+ const repoB = path.join(testRoot, "subdir-b");
441
+ await fs.mkdir(repoA);
442
+ await fs.mkdir(repoB);
443
+
444
+ // Initialize repo A with a file in a subdirectory
445
+ await fs.mkdir(path.join(repoA, "dir1"), { recursive: true });
446
+ await fs.writeFile(path.join(repoA, "dir1", "file1.txt"), "in dir1");
447
+
448
+ await pushwork(["init", "."], repoA);
449
+ await wait(500);
450
+
451
+ // Clone to B
452
+ const { stdout: rootUrl } = await pushwork(["url"], repoA);
453
+ await pushwork(["clone", rootUrl.trim(), repoB], tmpDir);
454
+ await wait(500);
455
+
456
+ // Verify B got the subdirectory and file
457
+ expect(await pathExists(path.join(repoB, "dir1", "file1.txt"))).toBe(
458
+ true
397
459
  );
398
- }
399
-
400
- const sync2 = await pushwork(["sync"], repoB);
401
- console.log(" B pushed?", sync2.stdout.includes("aaa.txt"));
402
- console.log(
403
- " B result:",
404
- sync2.stdout.includes("already in sync") ? "no changes" : "had changes"
405
- );
406
- console.log(" B full output:\n", sync2.stdout);
407
- await wait(1000);
408
-
409
- console.log("Round 3: A sync (should pull aaa.txt)...");
410
- const sync3 = await pushwork(["sync"], repoA);
411
- console.log(" A pulled?", sync3.stdout.includes("aaa.txt"));
412
- console.log(
413
- " A result:",
414
- sync3.stdout.includes("already in sync") ? "no changes" : "had changes"
415
- );
416
- await wait(1000);
417
-
418
- // Debug: Check what files exist
419
- const filesA = await fs.readdir(repoA);
420
- const filesB = await fs.readdir(repoB);
421
- console.log(
422
- "Files in A after sync:",
423
- filesA.filter((f) => !f.startsWith("."))
424
- );
425
- console.log(
426
- "Files in B after sync:",
427
- filesB.filter((f) => !f.startsWith("."))
428
- );
429
-
430
- // Check diff
431
- const { stdout: diffA } = await pushwork(["diff", "--name-only"], repoA);
432
- const { stdout: diffB } = await pushwork(["diff", "--name-only"], repoB);
433
- console.log("Diff A:", diffA.trim());
434
- console.log("Diff B:", diffB.trim());
435
-
436
- // Verify convergence
437
- const hashA = await hashDirectory(repoA);
438
- const hashB = await hashDirectory(repoB);
439
-
440
- console.log("Hash A:", hashA);
441
- console.log("Hash B:", hashB);
442
-
443
- expect(hashA).toBe(hashB);
444
-
445
- // Both should have aaa.txt
446
- expect(await pathExists(path.join(repoA, "aaa.txt"))).toBe(true);
447
- expect(await pathExists(path.join(repoB, "aaa.txt"))).toBe(true);
448
- }, 30000);
449
-
450
- it("should handle files in subdirectories and moves between directories", async () => {
451
- const repoA = path.join(tmpDir, "subdir-a");
452
- const repoB = path.join(tmpDir, "subdir-b");
453
- await fs.mkdir(repoA);
454
- await fs.mkdir(repoB);
455
-
456
- // Initialize repo A with a file in a subdirectory
457
- await fs.mkdir(path.join(repoA, "dir1"), { recursive: true });
458
- await fs.writeFile(path.join(repoA, "dir1", "file1.txt"), "in dir1");
459
-
460
- await pushwork(["init", "."], repoA);
461
- await wait(500);
462
-
463
- // Clone to B
464
- const { stdout: rootUrl } = await pushwork(["url"], repoA);
465
- await pushwork(["clone", rootUrl.trim(), repoB], tmpDir);
466
- await wait(500);
460
+ const initialContentB = await fs.readFile(
461
+ path.join(repoB, "dir1", "file1.txt"),
462
+ "utf-8"
463
+ );
464
+ expect(initialContentB).toBe("in dir1");
467
465
 
468
- // Verify B got the subdirectory and file
469
- expect(await pathExists(path.join(repoB, "dir1", "file1.txt"))).toBe(
470
- true
471
- );
472
- const initialContentB = await fs.readFile(
473
- path.join(repoB, "dir1", "file1.txt"),
474
- "utf-8"
475
- );
476
- expect(initialContentB).toBe("in dir1");
466
+ // On A: Create another file in a different subdirectory
467
+ await fs.mkdir(path.join(repoA, "dir2"), { recursive: true });
468
+ await fs.writeFile(path.join(repoA, "dir2", "file2.txt"), "in dir2");
477
469
 
478
- // On A: Create another file in a different subdirectory
479
- await fs.mkdir(path.join(repoA, "dir2"), { recursive: true });
480
- await fs.writeFile(path.join(repoA, "dir2", "file2.txt"), "in dir2");
470
+ // Sync both sides
471
+ await pushwork(["sync"], repoA);
472
+ await wait(1000);
473
+ await pushwork(["sync"], repoB);
474
+ await wait(1000);
481
475
 
482
- // Sync both sides
483
- await pushwork(["sync"], repoA);
484
- await wait(1000);
485
- await pushwork(["sync"], repoB);
486
- await wait(1000);
476
+ // Verify B got the new subdirectory and file
477
+ expect(await pathExists(path.join(repoB, "dir2", "file2.txt"))).toBe(
478
+ true
479
+ );
480
+ const file2ContentB = await fs.readFile(
481
+ path.join(repoB, "dir2", "file2.txt"),
482
+ "utf-8"
483
+ );
484
+ expect(file2ContentB).toBe("in dir2");
487
485
 
488
- // Verify B got the new subdirectory and file
489
- expect(await pathExists(path.join(repoB, "dir2", "file2.txt"))).toBe(
490
- true
491
- );
492
- const file2ContentB = await fs.readFile(
493
- path.join(repoB, "dir2", "file2.txt"),
494
- "utf-8"
495
- );
496
- expect(file2ContentB).toBe("in dir2");
497
- }, 30000);
486
+ // Cleanup
487
+ tmpObj.removeCallback();
488
+ },
489
+ 30000
490
+ );
498
491
  });
499
492
 
500
493
  describe("Property-Based Fuzzing with fast-check", () => {
@@ -684,24 +677,13 @@ describe("Pushwork Fuzzer", () => {
684
677
  await fs.mkdir(repoA);
685
678
  await fs.mkdir(repoB);
686
679
 
687
- const testStart = Date.now();
688
- console.log(
689
- `\n🔬 Testing: ${opsA.length} ops on A, ${opsB.length} ops on B`
690
- );
691
-
692
680
  try {
693
681
  // Initialize repo A with an initial file
694
- console.log(
695
- ` ⏱️ [${Date.now() - testStart}ms] Initializing repo A...`
696
- );
697
682
  await fs.writeFile(path.join(repoA, "initial.txt"), "initial");
698
683
  await pushwork(["init", "."], repoA);
699
684
  await wait(500);
700
685
 
701
686
  // Get root URL and clone to B
702
- console.log(
703
- ` ⏱️ [${Date.now() - testStart}ms] Cloning to repo B...`
704
- );
705
687
  const { stdout: rootUrl } = await pushwork(["url"], repoA);
706
688
  const cleanRootUrl = rootUrl.trim();
707
689
  await pushwork(["clone", cleanRootUrl, repoB], testRoot);
@@ -711,105 +693,43 @@ describe("Pushwork Fuzzer", () => {
711
693
  const hashBeforeOps = await hashDirectory(repoA);
712
694
  const hashB1 = await hashDirectory(repoB);
713
695
  expect(hashBeforeOps).toBe(hashB1);
714
- console.log(
715
- ` ⏱️ [${Date.now() - testStart}ms] Initial state verified`
716
- );
717
696
 
718
697
  // Apply operations to both sides
719
- console.log(
720
- ` ⏱️ [${Date.now() - testStart}ms] Applying ${
721
- opsA.length
722
- } operations to repo A...`
723
- );
724
- console.log(` Operations A: ${JSON.stringify(opsA)}`);
725
698
  await applyOperations(repoA, opsA);
726
699
 
727
- console.log(
728
- ` ⏱️ [${Date.now() - testStart}ms] Applying ${
729
- opsB.length
730
- } operations to repo B...`
731
- );
732
- console.log(` Operations B: ${JSON.stringify(opsB)}`);
733
700
  await applyOperations(repoB, opsB);
734
701
 
735
702
  // Multiple sync rounds for convergence
736
703
  // Need enough time for network propagation between CLI invocations
737
704
  // Round 1: A pushes changes
738
- console.log(
739
- ` ⏱️ [${Date.now() - testStart}ms] Sync round 1: A...`
740
- );
741
705
  await pushwork(["sync"], repoA);
742
- await wait(500);
706
+ await wait(1000);
743
707
 
744
708
  // Round 2: B pushes changes and pulls A's changes
745
- console.log(
746
- ` ⏱️ [${Date.now() - testStart}ms] Sync round 1: B...`
747
- );
748
709
  await pushwork(["sync"], repoB);
749
- await wait(500);
710
+ await wait(1000);
750
711
 
751
712
  // Round 3: A pulls B's changes
752
- console.log(
753
- ` ⏱️ [${Date.now() - testStart}ms] Sync round 2: A...`
754
- );
755
713
  await pushwork(["sync"], repoA);
756
- await wait(500);
714
+ await wait(1000);
757
715
 
758
716
  // Round 4: B confirms convergence
759
- console.log(
760
- ` ⏱️ [${Date.now() - testStart}ms] Sync round 2: B...`
761
- );
762
717
  await pushwork(["sync"], repoB);
763
- await wait(500);
718
+ await wait(1000);
764
719
 
765
720
  // Round 5: Final convergence check
766
- console.log(
767
- ` ⏱️ [${Date.now() - testStart}ms] Sync round 3: A (final)...`
768
- );
769
721
  await pushwork(["sync"], repoA);
770
- await wait(500);
722
+ await wait(1000);
771
723
 
772
724
  // Round 6: Extra convergence check (for aggressive fuzzing)
773
- console.log(
774
- ` ⏱️ [${Date.now() - testStart}ms] Sync round 3: B (final)...`
775
- );
776
725
  await pushwork(["sync"], repoB);
777
- await wait(500);
726
+ await wait(5000);
778
727
 
779
728
  // Verify final state matches
780
- console.log(
781
- ` ⏱️ [${Date.now() - testStart}ms] Verifying convergence...`
782
- );
783
729
 
784
730
  const hashAfterA = await hashDirectory(repoA);
785
731
  const hashAfterB = await hashDirectory(repoB);
786
732
 
787
- console.log(` Hash A: ${hashAfterA.substring(0, 16)}...`);
788
- console.log(` Hash B: ${hashAfterB.substring(0, 16)}...`);
789
-
790
- // Both sides should converge to the same state
791
- if (hashAfterA !== hashAfterB) {
792
- // Show what files are different
793
- const filesA = await getAllFiles(repoA);
794
- const filesB = await getAllFiles(repoB);
795
- console.log(` ❌ CONVERGENCE FAILURE!`);
796
- console.log(
797
- ` Files in A: ${filesA
798
- .filter((f) => !f.includes(".pushwork"))
799
- .join(", ")}`
800
- );
801
- console.log(
802
- ` Files in B: ${filesB
803
- .filter((f) => !f.includes(".pushwork"))
804
- .join(", ")}`
805
- );
806
- console.log(
807
- ` Operations applied to A: ${JSON.stringify(opsA)}`
808
- );
809
- console.log(
810
- ` Operations applied to B: ${JSON.stringify(opsB)}`
811
- );
812
- }
813
733
  expect(hashAfterA).toBe(hashAfterB);
814
734
 
815
735
  // Verify diff shows no changes
@@ -829,9 +749,6 @@ describe("Pushwork Fuzzer", () => {
829
749
  );
830
750
  expect(diffLines.length).toBe(0);
831
751
 
832
- const totalTime = Date.now() - testStart;
833
- console.log(` ✅ Converged successfully! (took ${totalTime}ms)`);
834
-
835
752
  // Cleanup
836
753
  await fs.rm(testRoot, { recursive: true, force: true });
837
754
  } catch (error) {
@@ -844,13 +761,13 @@ describe("Pushwork Fuzzer", () => {
844
761
  }
845
762
  ),
846
763
  {
847
- numRuns: 50, // INTENSE MODE (was 20, then cranked to 50)
848
- timeout: 180000, // 3 minute timeout per run
764
+ numRuns: 5, // INTENSE MODE (was 20, then cranked to 50)
765
+ timeout: 60000, // 1 minute timeout per run
849
766
  verbose: true, // Verbose output
850
767
  endOnFailure: true, // Stop on first failure to debug
851
768
  }
852
769
  );
853
- }, 1200000); // 20 minute timeout for the whole test
770
+ }, 600000); // 10 minute timeout for the whole test
854
771
  });
855
772
  });
856
773