pushwork 2.0.0-a.sub.0 → 2.0.0-preview

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 (251) hide show
  1. package/dist/branches.d.ts +19 -0
  2. package/dist/branches.d.ts.map +1 -0
  3. package/dist/branches.js +111 -0
  4. package/dist/branches.js.map +1 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +238 -272
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config.d.ts +17 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +84 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/fs-tree.d.ts +6 -0
  14. package/dist/fs-tree.d.ts.map +1 -0
  15. package/dist/fs-tree.js +99 -0
  16. package/dist/fs-tree.js.map +1 -0
  17. package/dist/ignore.d.ts +6 -0
  18. package/dist/ignore.d.ts.map +1 -0
  19. package/dist/ignore.js +74 -0
  20. package/dist/ignore.js.map +1 -0
  21. package/dist/index.d.ts +8 -4
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +34 -4
  24. package/dist/index.js.map +1 -1
  25. package/dist/log.d.ts +3 -0
  26. package/dist/log.d.ts.map +1 -0
  27. package/dist/log.js +14 -0
  28. package/dist/log.js.map +1 -0
  29. package/dist/pushwork.d.ts +115 -0
  30. package/dist/pushwork.d.ts.map +1 -0
  31. package/dist/pushwork.js +918 -0
  32. package/dist/pushwork.js.map +1 -0
  33. package/dist/repo.d.ts +14 -0
  34. package/dist/repo.d.ts.map +1 -0
  35. package/dist/repo.js +60 -0
  36. package/dist/repo.js.map +1 -0
  37. package/dist/shapes/custom.d.ts +3 -0
  38. package/dist/shapes/custom.d.ts.map +1 -0
  39. package/dist/shapes/custom.js +57 -0
  40. package/dist/shapes/custom.js.map +1 -0
  41. package/dist/shapes/file.d.ts +20 -0
  42. package/dist/shapes/file.d.ts.map +1 -0
  43. package/dist/shapes/file.js +140 -0
  44. package/dist/shapes/file.js.map +1 -0
  45. package/dist/shapes/index.d.ts +10 -0
  46. package/dist/shapes/index.d.ts.map +1 -0
  47. package/dist/shapes/index.js +35 -0
  48. package/dist/shapes/index.js.map +1 -0
  49. package/dist/shapes/patchwork-folder.d.ts +3 -0
  50. package/dist/shapes/patchwork-folder.d.ts.map +1 -0
  51. package/dist/shapes/patchwork-folder.js +160 -0
  52. package/dist/shapes/patchwork-folder.js.map +1 -0
  53. package/dist/shapes/types.d.ts +37 -0
  54. package/dist/shapes/types.d.ts.map +1 -0
  55. package/dist/shapes/types.js +52 -0
  56. package/dist/shapes/types.js.map +1 -0
  57. package/dist/shapes/vfs.d.ts +3 -0
  58. package/dist/shapes/vfs.d.ts.map +1 -0
  59. package/dist/shapes/vfs.js +88 -0
  60. package/dist/shapes/vfs.js.map +1 -0
  61. package/dist/stash.d.ts +23 -0
  62. package/dist/stash.d.ts.map +1 -0
  63. package/dist/stash.js +118 -0
  64. package/dist/stash.js.map +1 -0
  65. package/flake.lock +128 -0
  66. package/flake.nix +66 -0
  67. package/package.json +15 -48
  68. package/patches/@automerge__automerge-repo@2.6.0-subduction.15.patch +26 -0
  69. package/pnpm-workspace.yaml +5 -0
  70. package/src/branches.ts +93 -0
  71. package/src/cli.ts +258 -408
  72. package/src/config.ts +64 -0
  73. package/src/fs-tree.ts +70 -0
  74. package/src/ignore.ts +33 -0
  75. package/src/index.ts +38 -4
  76. package/src/log.ts +8 -0
  77. package/src/pushwork.ts +1055 -0
  78. package/src/repo.ts +76 -0
  79. package/src/shapes/custom.ts +29 -0
  80. package/src/shapes/file.ts +115 -0
  81. package/src/shapes/index.ts +19 -0
  82. package/src/shapes/patchwork-folder.ts +156 -0
  83. package/src/shapes/types.ts +79 -0
  84. package/src/shapes/vfs.ts +93 -0
  85. package/src/stash.ts +106 -0
  86. package/test/integration/branches.test.ts +389 -0
  87. package/test/integration/pushwork.test.ts +547 -0
  88. package/test/setup.ts +29 -0
  89. package/test/unit/doc-shape.test.ts +612 -0
  90. package/tsconfig.json +2 -3
  91. package/vitest.config.ts +14 -0
  92. package/ARCHITECTURE-ACCORDING-TO-CLAUDE.md +0 -248
  93. package/CLAUDE.md +0 -141
  94. package/README.md +0 -221
  95. package/babel.config.js +0 -5
  96. package/dist/cli/commands.d.ts +0 -71
  97. package/dist/cli/commands.d.ts.map +0 -1
  98. package/dist/cli/commands.js +0 -794
  99. package/dist/cli/commands.js.map +0 -1
  100. package/dist/cli/index.d.ts +0 -2
  101. package/dist/cli/index.d.ts.map +0 -1
  102. package/dist/cli/index.js +0 -19
  103. package/dist/cli/index.js.map +0 -1
  104. package/dist/commands.d.ts +0 -61
  105. package/dist/commands.d.ts.map +0 -1
  106. package/dist/commands.js +0 -861
  107. package/dist/commands.js.map +0 -1
  108. package/dist/config/index.d.ts +0 -71
  109. package/dist/config/index.d.ts.map +0 -1
  110. package/dist/config/index.js +0 -314
  111. package/dist/config/index.js.map +0 -1
  112. package/dist/core/change-detection.d.ts +0 -80
  113. package/dist/core/change-detection.d.ts.map +0 -1
  114. package/dist/core/change-detection.js +0 -523
  115. package/dist/core/change-detection.js.map +0 -1
  116. package/dist/core/config.d.ts +0 -81
  117. package/dist/core/config.d.ts.map +0 -1
  118. package/dist/core/config.js +0 -258
  119. package/dist/core/config.js.map +0 -1
  120. package/dist/core/index.d.ts +0 -6
  121. package/dist/core/index.d.ts.map +0 -1
  122. package/dist/core/index.js +0 -6
  123. package/dist/core/index.js.map +0 -1
  124. package/dist/core/move-detection.d.ts +0 -34
  125. package/dist/core/move-detection.d.ts.map +0 -1
  126. package/dist/core/move-detection.js +0 -121
  127. package/dist/core/move-detection.js.map +0 -1
  128. package/dist/core/snapshot.d.ts +0 -105
  129. package/dist/core/snapshot.d.ts.map +0 -1
  130. package/dist/core/snapshot.js +0 -217
  131. package/dist/core/snapshot.js.map +0 -1
  132. package/dist/core/sync-engine.d.ts +0 -151
  133. package/dist/core/sync-engine.d.ts.map +0 -1
  134. package/dist/core/sync-engine.js +0 -1346
  135. package/dist/core/sync-engine.js.map +0 -1
  136. package/dist/types/config.d.ts +0 -99
  137. package/dist/types/config.d.ts.map +0 -1
  138. package/dist/types/config.js +0 -5
  139. package/dist/types/config.js.map +0 -1
  140. package/dist/types/documents.d.ts +0 -88
  141. package/dist/types/documents.d.ts.map +0 -1
  142. package/dist/types/documents.js +0 -20
  143. package/dist/types/documents.js.map +0 -1
  144. package/dist/types/index.d.ts +0 -4
  145. package/dist/types/index.d.ts.map +0 -1
  146. package/dist/types/index.js +0 -4
  147. package/dist/types/index.js.map +0 -1
  148. package/dist/types/snapshot.d.ts +0 -64
  149. package/dist/types/snapshot.d.ts.map +0 -1
  150. package/dist/types/snapshot.js +0 -2
  151. package/dist/types/snapshot.js.map +0 -1
  152. package/dist/utils/content-similarity.d.ts +0 -53
  153. package/dist/utils/content-similarity.d.ts.map +0 -1
  154. package/dist/utils/content-similarity.js +0 -155
  155. package/dist/utils/content-similarity.js.map +0 -1
  156. package/dist/utils/content.d.ts +0 -10
  157. package/dist/utils/content.d.ts.map +0 -1
  158. package/dist/utils/content.js +0 -31
  159. package/dist/utils/content.js.map +0 -1
  160. package/dist/utils/directory.d.ts +0 -24
  161. package/dist/utils/directory.d.ts.map +0 -1
  162. package/dist/utils/directory.js +0 -52
  163. package/dist/utils/directory.js.map +0 -1
  164. package/dist/utils/fs.d.ts +0 -74
  165. package/dist/utils/fs.d.ts.map +0 -1
  166. package/dist/utils/fs.js +0 -248
  167. package/dist/utils/fs.js.map +0 -1
  168. package/dist/utils/index.d.ts +0 -5
  169. package/dist/utils/index.d.ts.map +0 -1
  170. package/dist/utils/index.js +0 -5
  171. package/dist/utils/index.js.map +0 -1
  172. package/dist/utils/mime-types.d.ts +0 -13
  173. package/dist/utils/mime-types.d.ts.map +0 -1
  174. package/dist/utils/mime-types.js +0 -209
  175. package/dist/utils/mime-types.js.map +0 -1
  176. package/dist/utils/network-sync.d.ts +0 -36
  177. package/dist/utils/network-sync.d.ts.map +0 -1
  178. package/dist/utils/network-sync.js +0 -250
  179. package/dist/utils/network-sync.js.map +0 -1
  180. package/dist/utils/node-polyfills.d.ts +0 -9
  181. package/dist/utils/node-polyfills.d.ts.map +0 -1
  182. package/dist/utils/node-polyfills.js +0 -9
  183. package/dist/utils/node-polyfills.js.map +0 -1
  184. package/dist/utils/output.d.ts +0 -129
  185. package/dist/utils/output.d.ts.map +0 -1
  186. package/dist/utils/output.js +0 -368
  187. package/dist/utils/output.js.map +0 -1
  188. package/dist/utils/repo-factory.d.ts +0 -13
  189. package/dist/utils/repo-factory.d.ts.map +0 -1
  190. package/dist/utils/repo-factory.js +0 -46
  191. package/dist/utils/repo-factory.js.map +0 -1
  192. package/dist/utils/string-similarity.d.ts +0 -14
  193. package/dist/utils/string-similarity.d.ts.map +0 -1
  194. package/dist/utils/string-similarity.js +0 -39
  195. package/dist/utils/string-similarity.js.map +0 -1
  196. package/dist/utils/text-diff.d.ts +0 -37
  197. package/dist/utils/text-diff.d.ts.map +0 -1
  198. package/dist/utils/text-diff.js +0 -93
  199. package/dist/utils/text-diff.js.map +0 -1
  200. package/dist/utils/trace.d.ts +0 -19
  201. package/dist/utils/trace.d.ts.map +0 -1
  202. package/dist/utils/trace.js +0 -63
  203. package/dist/utils/trace.js.map +0 -1
  204. package/src/commands.ts +0 -1134
  205. package/src/core/change-detection.ts +0 -712
  206. package/src/core/config.ts +0 -313
  207. package/src/core/index.ts +0 -5
  208. package/src/core/move-detection.ts +0 -169
  209. package/src/core/snapshot.ts +0 -275
  210. package/src/core/sync-engine.ts +0 -1758
  211. package/src/types/config.ts +0 -111
  212. package/src/types/documents.ts +0 -91
  213. package/src/types/index.ts +0 -3
  214. package/src/types/snapshot.ts +0 -67
  215. package/src/utils/content.ts +0 -34
  216. package/src/utils/directory.ts +0 -73
  217. package/src/utils/fs.ts +0 -297
  218. package/src/utils/index.ts +0 -4
  219. package/src/utils/mime-types.ts +0 -244
  220. package/src/utils/network-sync.ts +0 -319
  221. package/src/utils/node-polyfills.ts +0 -8
  222. package/src/utils/output.ts +0 -450
  223. package/src/utils/repo-factory.ts +0 -73
  224. package/src/utils/string-similarity.ts +0 -54
  225. package/src/utils/text-diff.ts +0 -101
  226. package/src/utils/trace.ts +0 -70
  227. package/test/integration/README.md +0 -328
  228. package/test/integration/clone-test.sh +0 -310
  229. package/test/integration/conflict-resolution-test.sh +0 -309
  230. package/test/integration/debug-both-nested.sh +0 -74
  231. package/test/integration/debug-concurrent-nested.sh +0 -87
  232. package/test/integration/debug-nested.sh +0 -73
  233. package/test/integration/deletion-behavior-test.sh +0 -487
  234. package/test/integration/deletion-sync-test-simple.sh +0 -193
  235. package/test/integration/deletion-sync-test.sh +0 -297
  236. package/test/integration/exclude-patterns.test.ts +0 -144
  237. package/test/integration/full-integration-test.sh +0 -363
  238. package/test/integration/fuzzer.test.ts +0 -818
  239. package/test/integration/in-memory-sync.test.ts +0 -830
  240. package/test/integration/init-sync.test.ts +0 -89
  241. package/test/integration/manual-sync-test.sh +0 -84
  242. package/test/integration/sync-deletion.test.ts +0 -280
  243. package/test/integration/sync-flow.test.ts +0 -291
  244. package/test/jest.setup.ts +0 -34
  245. package/test/run-tests.sh +0 -225
  246. package/test/unit/deletion-behavior.test.ts +0 -249
  247. package/test/unit/enhanced-mime-detection.test.ts +0 -244
  248. package/test/unit/snapshot.test.ts +0 -404
  249. package/test/unit/sync-convergence.test.ts +0 -298
  250. package/test/unit/sync-timing.test.ts +0 -134
  251. package/test/unit/utils.test.ts +0 -366
@@ -1,404 +0,0 @@
1
- import * as path from "path";
2
- import * as tmp from "tmp";
3
- import { SnapshotManager } from "../../src/core/snapshot";
4
- import { SnapshotFileEntry, SnapshotDirectoryEntry } from "../../src/types";
5
- import { UrlHeads } from "@automerge/automerge-repo";
6
-
7
- describe("SnapshotManager", () => {
8
- let tmpDir: string;
9
- let cleanup: () => void;
10
- let snapshotManager: SnapshotManager;
11
-
12
- beforeEach(() => {
13
- const tmpObj = tmp.dirSync({ unsafeCleanup: true });
14
- tmpDir = tmpObj.name;
15
- cleanup = tmpObj.removeCallback;
16
- snapshotManager = new SnapshotManager(tmpDir);
17
- });
18
-
19
- afterEach(() => {
20
- cleanup();
21
- });
22
-
23
- describe("exists", () => {
24
- it("should return false when no snapshot exists", async () => {
25
- expect(await snapshotManager.exists()).toBe(false);
26
- });
27
-
28
- it("should return true when snapshot exists", async () => {
29
- const snapshot = snapshotManager.createEmpty();
30
- await snapshotManager.save(snapshot);
31
-
32
- expect(await snapshotManager.exists()).toBe(true);
33
- });
34
- });
35
-
36
- describe("createEmpty", () => {
37
- it("should create an empty snapshot", () => {
38
- const snapshot = snapshotManager.createEmpty();
39
-
40
- expect(snapshot.rootPath).toBe(tmpDir);
41
- expect(snapshot.timestamp).toBeGreaterThan(0);
42
- expect(snapshot.files.size).toBe(0);
43
- expect(snapshot.directories.size).toBe(0);
44
- });
45
- });
46
-
47
- describe("save and load", () => {
48
- it("should save and load empty snapshot", async () => {
49
- const originalSnapshot = snapshotManager.createEmpty();
50
-
51
- await snapshotManager.save(originalSnapshot);
52
- const loadedSnapshot = await snapshotManager.load();
53
-
54
- expect(loadedSnapshot).not.toBeNull();
55
- expect(loadedSnapshot!.rootPath).toBe(originalSnapshot.rootPath);
56
- expect(loadedSnapshot!.timestamp).toBe(originalSnapshot.timestamp);
57
- expect(loadedSnapshot!.files.size).toBe(0);
58
- expect(loadedSnapshot!.directories.size).toBe(0);
59
- });
60
-
61
- it("should save and load snapshot with files", async () => {
62
- const snapshot = snapshotManager.createEmpty();
63
-
64
- const fileEntry: SnapshotFileEntry = {
65
- path: path.join(tmpDir, "test.txt"),
66
- url: "automerge:test-url" as any,
67
- head: ["test-head"] as UrlHeads,
68
- extension: "txt",
69
- mimeType: "text/plain",
70
- };
71
-
72
- snapshotManager.updateFileEntry(snapshot, "test.txt", fileEntry);
73
-
74
- await snapshotManager.save(snapshot);
75
- const loadedSnapshot = await snapshotManager.load();
76
-
77
- expect(loadedSnapshot).not.toBeNull();
78
- expect(loadedSnapshot!.files.size).toBe(1);
79
- expect(loadedSnapshot!.files.get("test.txt")).toEqual(fileEntry);
80
- });
81
-
82
- it("should save and load snapshot with directories", async () => {
83
- const snapshot = snapshotManager.createEmpty();
84
-
85
- const dirEntry: SnapshotDirectoryEntry = {
86
- path: path.join(tmpDir, "subdir"),
87
- url: "automerge:dir-url" as any,
88
- head: ["dir-head"] as UrlHeads,
89
- entries: ["file1.txt", "file2.txt"],
90
- };
91
-
92
- snapshotManager.updateDirectoryEntry(snapshot, "subdir", dirEntry);
93
-
94
- await snapshotManager.save(snapshot);
95
- const loadedSnapshot = await snapshotManager.load();
96
-
97
- expect(loadedSnapshot).not.toBeNull();
98
- expect(loadedSnapshot!.directories.size).toBe(1);
99
- expect(loadedSnapshot!.directories.get("subdir")).toEqual(dirEntry);
100
- });
101
-
102
- it("should return null when loading non-existent snapshot", async () => {
103
- const loadedSnapshot = await snapshotManager.load();
104
- expect(loadedSnapshot).toBeNull();
105
- });
106
- });
107
-
108
- describe("updateFileEntry", () => {
109
- it("should add new file entry", () => {
110
- const snapshot = snapshotManager.createEmpty();
111
- const originalTimestamp = snapshot.timestamp;
112
-
113
- const fileEntry: SnapshotFileEntry = {
114
- path: "/test/path/test.txt",
115
- url: "automerge:test-url" as any,
116
- head: ["test-head"] as UrlHeads,
117
- extension: "txt",
118
- mimeType: "text/plain",
119
- };
120
-
121
- // Add small delay to ensure timestamp changes
122
- const startTime = Date.now();
123
- while (Date.now() === startTime) {
124
- // Wait for at least 1ms
125
- }
126
-
127
- snapshotManager.updateFileEntry(snapshot, "test.txt", fileEntry);
128
-
129
- expect(snapshot.files.get("test.txt")).toEqual(fileEntry);
130
- expect(snapshot.timestamp).toBeGreaterThan(originalTimestamp);
131
- });
132
-
133
- it("should update existing file entry", () => {
134
- const snapshot = snapshotManager.createEmpty();
135
-
136
- const fileEntry1: SnapshotFileEntry = {
137
- path: path.join(tmpDir, "test.txt"),
138
- url: "automerge:test-url" as any,
139
- head: ["old-head"] as UrlHeads,
140
- extension: "txt",
141
- mimeType: "text/plain",
142
- };
143
-
144
- const fileEntry2: SnapshotFileEntry = {
145
- path: path.join(tmpDir, "test.txt"),
146
- url: "automerge:test-url" as any,
147
- head: ["new-head"] as UrlHeads,
148
- extension: "txt",
149
- mimeType: "text/plain",
150
- };
151
-
152
- snapshotManager.updateFileEntry(snapshot, "test.txt", fileEntry1);
153
- snapshotManager.updateFileEntry(snapshot, "test.txt", fileEntry2);
154
-
155
- expect(snapshot.files.get("test.txt")).toEqual(fileEntry2);
156
- expect(snapshot.files.size).toBe(1);
157
- });
158
- });
159
-
160
- describe("removeFileEntry", () => {
161
- it("should remove file entry", () => {
162
- const snapshot = snapshotManager.createEmpty();
163
-
164
- const fileEntry: SnapshotFileEntry = {
165
- path: path.join(tmpDir, "test.txt"),
166
- url: "automerge:test-url" as any,
167
- head: ["test-head"] as UrlHeads,
168
- extension: "txt",
169
- mimeType: "text/plain",
170
- };
171
-
172
- snapshotManager.updateFileEntry(snapshot, "test.txt", fileEntry);
173
- expect(snapshot.files.size).toBe(1);
174
-
175
- snapshotManager.removeFileEntry(snapshot, "test.txt");
176
- expect(snapshot.files.size).toBe(0);
177
- expect(snapshot.files.get("test.txt")).toBeUndefined();
178
- });
179
-
180
- it("should not fail when removing non-existent file", () => {
181
- const snapshot = snapshotManager.createEmpty();
182
-
183
- snapshotManager.removeFileEntry(snapshot, "nonexistent.txt");
184
- expect(snapshot.files.size).toBe(0);
185
- });
186
- });
187
-
188
- describe("getFilePaths and getDirectoryPaths", () => {
189
- it("should return all file paths", () => {
190
- const snapshot = snapshotManager.createEmpty();
191
-
192
- snapshotManager.updateFileEntry(snapshot, "file1.txt", {
193
- path: path.join(tmpDir, "file1.txt"),
194
- url: "automerge:url1" as any,
195
- head: ["head1"] as UrlHeads,
196
- extension: "txt",
197
- mimeType: "text/plain",
198
- });
199
-
200
- snapshotManager.updateFileEntry(snapshot, "file2.txt", {
201
- path: path.join(tmpDir, "file2.txt"),
202
- url: "automerge:url2" as any,
203
- head: ["head2"] as UrlHeads,
204
- extension: "txt",
205
- mimeType: "text/plain",
206
- });
207
-
208
- const filePaths = snapshotManager.getFilePaths(snapshot);
209
- expect(filePaths.sort()).toEqual(["file1.txt", "file2.txt"]);
210
- });
211
-
212
- it("should return all directory paths", () => {
213
- const snapshot = snapshotManager.createEmpty();
214
-
215
- snapshotManager.updateDirectoryEntry(snapshot, "dir1", {
216
- path: path.join(tmpDir, "dir1"),
217
- url: "automerge:url1" as any,
218
- head: ["head1"] as UrlHeads,
219
- entries: [],
220
- });
221
-
222
- snapshotManager.updateDirectoryEntry(snapshot, "dir2", {
223
- path: path.join(tmpDir, "dir2"),
224
- url: "automerge:url2" as any,
225
- head: ["head2"] as UrlHeads,
226
- entries: [],
227
- });
228
-
229
- const dirPaths = snapshotManager.getDirectoryPaths(snapshot);
230
- expect(dirPaths.sort()).toEqual(["dir1", "dir2"]);
231
- });
232
- });
233
-
234
- describe("isTracked", () => {
235
- it("should return true for tracked files", () => {
236
- const snapshot = snapshotManager.createEmpty();
237
-
238
- snapshotManager.updateFileEntry(snapshot, "test.txt", {
239
- path: path.join(tmpDir, "test.txt"),
240
- url: "automerge:url" as any,
241
- head: ["head"] as UrlHeads,
242
- extension: "txt",
243
- mimeType: "text/plain",
244
- });
245
-
246
- expect(snapshotManager.isTracked(snapshot, "test.txt")).toBe(true);
247
- expect(snapshotManager.isTracked(snapshot, "other.txt")).toBe(false);
248
- });
249
-
250
- it("should return true for tracked directories", () => {
251
- const snapshot = snapshotManager.createEmpty();
252
-
253
- snapshotManager.updateDirectoryEntry(snapshot, "subdir", {
254
- path: path.join(tmpDir, "subdir"),
255
- url: "automerge:url" as any,
256
- head: ["head"] as UrlHeads,
257
- entries: [],
258
- });
259
-
260
- expect(snapshotManager.isTracked(snapshot, "subdir")).toBe(true);
261
- expect(snapshotManager.isTracked(snapshot, "other")).toBe(false);
262
- });
263
- });
264
-
265
- describe("getStats", () => {
266
- it("should return correct statistics", () => {
267
- const snapshot = snapshotManager.createEmpty();
268
-
269
- snapshotManager.updateFileEntry(snapshot, "file1.txt", {
270
- path: path.join(tmpDir, "file1.txt"),
271
- url: "automerge:url1" as any,
272
- head: ["head1"] as UrlHeads,
273
- extension: "txt",
274
- mimeType: "text/plain",
275
- });
276
-
277
- snapshotManager.updateDirectoryEntry(snapshot, "dir1", {
278
- path: path.join(tmpDir, "dir1"),
279
- url: "automerge:url2" as any,
280
- head: ["head2"] as UrlHeads,
281
- entries: [],
282
- });
283
-
284
- const stats = snapshotManager.getStats(snapshot);
285
-
286
- expect(stats.files).toBe(1);
287
- expect(stats.directories).toBe(1);
288
- expect(stats.timestamp).toBeInstanceOf(Date);
289
- expect(stats.timestamp.getTime()).toBe(snapshot.timestamp);
290
- });
291
- });
292
-
293
- describe("validate", () => {
294
- it("should validate correct snapshot", () => {
295
- const snapshot = snapshotManager.createEmpty();
296
-
297
- const validation = snapshotManager.validate(snapshot);
298
-
299
- expect(validation.valid).toBe(true);
300
- expect(validation.errors).toHaveLength(0);
301
- });
302
-
303
- it("should detect invalid timestamp", () => {
304
- const snapshot = snapshotManager.createEmpty();
305
- snapshot.timestamp = 0;
306
-
307
- const validation = snapshotManager.validate(snapshot);
308
-
309
- expect(validation.valid).toBe(false);
310
- expect(validation.errors).toContain("Invalid timestamp");
311
- });
312
-
313
- it("should detect missing root path", () => {
314
- const snapshot = snapshotManager.createEmpty();
315
- snapshot.rootPath = "";
316
-
317
- const validation = snapshotManager.validate(snapshot);
318
-
319
- expect(validation.valid).toBe(false);
320
- expect(validation.errors).toContain("Missing root path");
321
- });
322
-
323
- it("should detect path conflicts", () => {
324
- const snapshot = snapshotManager.createEmpty();
325
-
326
- snapshotManager.updateFileEntry(snapshot, "conflict", {
327
- path: path.join(tmpDir, "conflict"),
328
- url: "automerge:url1" as any,
329
- head: ["head1"] as UrlHeads,
330
- extension: "",
331
- mimeType: "text/plain",
332
- });
333
-
334
- snapshotManager.updateDirectoryEntry(snapshot, "conflict", {
335
- path: path.join(tmpDir, "conflict"),
336
- url: "automerge:url2" as any,
337
- head: ["head2"] as UrlHeads,
338
- entries: [],
339
- });
340
-
341
- const validation = snapshotManager.validate(snapshot);
342
-
343
- expect(validation.valid).toBe(false);
344
- expect(validation.errors).toContain(
345
- "Path conflict: conflict exists as both file and directory"
346
- );
347
- });
348
- });
349
-
350
- describe("clone", () => {
351
- it("should create independent copy of snapshot", () => {
352
- const originalSnapshot = snapshotManager.createEmpty();
353
-
354
- snapshotManager.updateFileEntry(originalSnapshot, "test.txt", {
355
- path: path.join(tmpDir, "test.txt"),
356
- url: "automerge:url" as any,
357
- head: ["head"] as UrlHeads,
358
- extension: "txt",
359
- mimeType: "text/plain",
360
- });
361
-
362
- const clonedSnapshot = snapshotManager.clone(originalSnapshot);
363
-
364
- // Modify clone
365
- snapshotManager.removeFileEntry(clonedSnapshot, "test.txt");
366
-
367
- // Original should be unchanged
368
- expect(originalSnapshot.files.size).toBe(1);
369
- expect(clonedSnapshot.files.size).toBe(0);
370
- });
371
- });
372
-
373
- describe("clear", () => {
374
- it("should clear all data from snapshot", async () => {
375
- const snapshot = snapshotManager.createEmpty();
376
-
377
- snapshotManager.updateFileEntry(snapshot, "test.txt", {
378
- path: path.join(tmpDir, "test.txt"),
379
- url: "automerge:url" as any,
380
- head: ["head"] as UrlHeads,
381
- extension: "txt",
382
- mimeType: "text/plain",
383
- });
384
-
385
- snapshotManager.updateDirectoryEntry(snapshot, "subdir", {
386
- path: path.join(tmpDir, "subdir"),
387
- url: "automerge:url" as any,
388
- head: ["head"] as UrlHeads,
389
- entries: [],
390
- });
391
-
392
- const originalTimestamp = snapshot.timestamp;
393
-
394
- // Add small delay to ensure timestamp difference
395
- await new Promise((resolve) => setTimeout(resolve, 10));
396
-
397
- snapshotManager.clear(snapshot);
398
-
399
- expect(snapshot.files.size).toBe(0);
400
- expect(snapshot.directories.size).toBe(0);
401
- expect(snapshot.timestamp).toBeGreaterThan(originalTimestamp);
402
- });
403
- });
404
- });
@@ -1,298 +0,0 @@
1
- import * as fs from "fs/promises";
2
- import * as path from "path";
3
- import { tmpdir } from "os";
4
- import { SnapshotManager } from "../../src/core/snapshot";
5
- import { ChangeDetector } from "../../src/core/change-detection";
6
- import { MoveDetector } from "../../src/core/move-detection";
7
- import { writeFileContent, removePath, pathExists } from "../../src/utils";
8
-
9
- describe("Sync Convergence Issues", () => {
10
- let testDir: string;
11
- let snapshotManager: SnapshotManager;
12
-
13
- beforeEach(async () => {
14
- testDir = await fs.mkdtemp(path.join(tmpdir(), "sync-convergence-test-"));
15
- snapshotManager = new SnapshotManager(testDir);
16
-
17
- // Create mock repo for change detector - we'll focus on change detection logic
18
- const mockRepo = {} as any;
19
- new ChangeDetector(mockRepo, testDir, []);
20
- new MoveDetector();
21
- });
22
-
23
- afterEach(async () => {
24
- await fs.rm(testDir, { recursive: true, force: true });
25
- });
26
-
27
- describe("Change Detection Patterns", () => {
28
- it("should verify that convergence issues are fixed", async () => {
29
- // === SETUP PHASE ===
30
-
31
- // Create initial file structure similar to Vite build output
32
- const initialFiles = [
33
- {
34
- name: "assets/tool-DhQI94EZ.js",
35
- content: "// Initial tool bundle\nexport const tool = 'v1';",
36
- },
37
- {
38
- name: "assets/index-BKR4T14z.js",
39
- content: "// Index bundle\nexport const app = 'main';",
40
- },
41
- {
42
- name: "index.js",
43
- content: "// Main entry\nimport './assets/tool-DhQI94EZ.js';",
44
- },
45
- ];
46
-
47
- for (const file of initialFiles) {
48
- const filePath = path.join(testDir, file.name);
49
- await fs.mkdir(path.dirname(filePath), { recursive: true });
50
- await writeFileContent(filePath, file.content);
51
- }
52
-
53
- // Create initial snapshot representing the "synced" state
54
- const snapshot = snapshotManager.createEmpty();
55
-
56
- // Simulate files being tracked in snapshot with mock URLs and heads
57
- for (const file of initialFiles) {
58
- snapshotManager.updateFileEntry(snapshot, file.name, {
59
- path: path.join(testDir, file.name),
60
- url: `automerge:mock-${file.name.replace(/[\/\.]/g, "-")}` as any,
61
- head: [`mock-head-${file.name}`] as any,
62
- extension: path.extname(file.name).slice(1) || "js",
63
- mimeType: "text/javascript",
64
- });
65
- }
66
-
67
- // === SIMULATE BUILD PROCESS ===
68
-
69
- // Delete old file and create new one (simulating Vite's content-based naming)
70
- await removePath(path.join(testDir, "assets/tool-DhQI94EZ.js"));
71
-
72
- const newToolFile = "assets/tool-CR5n6i_K.js";
73
- await writeFileContent(
74
- path.join(testDir, newToolFile),
75
- "// New tool bundle with different hash\nexport const tool = 'v2';"
76
- );
77
-
78
- // Update the main file to reference the new bundle
79
- await writeFileContent(
80
- path.join(testDir, "index.js"),
81
- "// Main entry\nimport './assets/tool-CR5n6i_K.js';"
82
- );
83
-
84
- // This is where we would normally detect changes, but we'll simulate the issue
85
- // by showing what the change detector would find vs what should happen
86
-
87
- // Simulate what change detection finds
88
- const deletedFile = "assets/tool-DhQI94EZ.js";
89
- const createdFile = "assets/tool-CR5n6i_K.js";
90
-
91
- // Simulate multiple "sync runs" by checking filesystem state
92
- let syncRun = 1;
93
- let changesRemaining = true;
94
-
95
- while (changesRemaining && syncRun <= 3) {
96
- // Check what should be synced
97
- const fileExists = await pathExists(path.join(testDir, deletedFile));
98
- const isTrackedInSnapshot = snapshot.files.has(deletedFile);
99
-
100
- if (!fileExists && isTrackedInSnapshot) {
101
- // In a real scenario with the bug, this deletion might not complete properly
102
- // due to stale directory heads, causing it to remain in the directory document
103
-
104
- // Simulate partial success - remove from snapshot but directory doc might still reference it
105
- snapshotManager.removeFileEntry(snapshot, deletedFile);
106
- }
107
-
108
- const newFileExists = await pathExists(path.join(testDir, createdFile));
109
- const newFileTracked = snapshot.files.has(createdFile);
110
-
111
- if (newFileExists && !newFileTracked) {
112
- // Add new file to snapshot
113
- snapshotManager.updateFileEntry(snapshot, createdFile, {
114
- path: path.join(testDir, createdFile),
115
- url: `automerge:mock-${createdFile.replace(/[\/\.]/g, "-")}` as any,
116
- head: [`mock-head-${createdFile}`] as any,
117
- extension: "js",
118
- mimeType: "text/javascript",
119
- });
120
- }
121
-
122
- // Check if we still have work to do
123
- // With the fix: Directory heads are properly updated, so convergence happens in 1 run
124
- if (syncRun === 1) {
125
- changesRemaining = false; // Fixed behavior: converge immediately
126
- } else {
127
- // This shouldn't happen with the fix
128
- changesRemaining = false;
129
- }
130
-
131
- syncRun++;
132
- }
133
-
134
- expect(syncRun - 1).toBe(1);
135
-
136
- // Verify final filesystem state is correct regardless of sync issues
137
- expect(
138
- await pathExists(path.join(testDir, "assets/tool-DhQI94EZ.js"))
139
- ).toBe(false);
140
- expect(
141
- await pathExists(path.join(testDir, "assets/tool-CR5n6i_K.js"))
142
- ).toBe(true);
143
- expect(await pathExists(path.join(testDir, "index.js"))).toBe(true);
144
-
145
- // Verify snapshot state
146
- expect(snapshot.files.has("assets/tool-DhQI94EZ.js")).toBe(false);
147
- expect(snapshot.files.has("assets/tool-CR5n6i_K.js")).toBe(true);
148
-
149
- expect(syncRun - 1).toBe(1); // Fixed behavior: exactly 1 run
150
- });
151
-
152
- it("should demonstrate snapshot head tracking concepts", async () => {
153
- // Create a simple file structure
154
- await fs.mkdir(path.join(testDir, "subdir"), { recursive: true });
155
- await writeFileContent(
156
- path.join(testDir, "subdir/test.js"),
157
- "console.log('test');"
158
- );
159
-
160
- // Create snapshot
161
- const snapshot = snapshotManager.createEmpty();
162
-
163
- // Add directory entry with initial "heads"
164
- snapshotManager.updateDirectoryEntry(snapshot, "subdir", {
165
- path: path.join(testDir, "subdir"),
166
- url: "automerge:mock-subdir" as any,
167
- head: ["initial-head"] as any, // This represents the initial state
168
- entries: [],
169
- });
170
-
171
- // Add file entry
172
- snapshotManager.updateFileEntry(snapshot, "subdir/test.js", {
173
- path: path.join(testDir, "subdir/test.js"),
174
- url: "automerge:mock-file" as any,
175
- head: ["file-head"] as any,
176
- extension: "js",
177
- mimeType: "text/javascript",
178
- });
179
-
180
- // === SIMULATE THE HEAD TRACKING ISSUE ===
181
-
182
- // Delete the file locally
183
- await removePath(path.join(testDir, "subdir/test.js"));
184
-
185
- // In a real sync scenario, we would:
186
- // 1. Detect the file deletion
187
- // 2. Remove file from directory document using current heads
188
- // 3. Update snapshot with new heads
189
-
190
- // THE BUG: Step 3 might not happen properly, causing stale heads
191
-
192
- // Simulate what should happen (correct behavior)
193
- snapshotManager.removeFileEntry(snapshot, "subdir/test.js");
194
-
195
- // Simulate directory heads advancing after modification
196
- const directoryEntry = snapshot.directories.get("subdir");
197
- if (directoryEntry) {
198
- // In real sync, heads would advance: ["initial-head"] -> ["new-head-after-deletion"]
199
- const newHeads = ["new-head-after-deletion"];
200
-
201
- directoryEntry.head = newHeads as any;
202
- }
203
-
204
- // Verify the concept
205
- const fileStillExists = await pathExists(
206
- path.join(testDir, "subdir/test.js")
207
- );
208
- const fileStillTracked = snapshot.files.has("subdir/test.js");
209
-
210
- expect(fileStillExists).toBe(false);
211
- expect(fileStillTracked).toBe(false);
212
- });
213
- });
214
-
215
- describe("Move Detection Interaction", () => {
216
- it("should show how move detection affects convergence behavior", async () => {
217
- // Create initial file
218
- await writeFileContent(
219
- path.join(testDir, "original.js"),
220
- "console.log('original');"
221
- );
222
-
223
- // Create snapshot tracking the original file
224
- const snapshot = snapshotManager.createEmpty();
225
- snapshotManager.updateFileEntry(snapshot, "original.js", {
226
- path: path.join(testDir, "original.js"),
227
- url: "automerge:original" as any,
228
- head: ["original-head"] as any,
229
- extension: "js",
230
- mimeType: "text/javascript",
231
- });
232
-
233
- // === SIMULATE RENAME WITH LOW SIMILARITY ===
234
-
235
- // Delete original and create "renamed" file with different content (low similarity)
236
- await removePath(path.join(testDir, "original.js"));
237
- await writeFileContent(
238
- path.join(testDir, "renamed.js"),
239
- "// Completely different content\nconst newFeature = () => { return 'different'; };"
240
- );
241
-
242
- // Since move detection doesn't apply, we process as delete + create
243
- // This should ALWAYS converge in exactly 1 sync run, but the bug causes more
244
-
245
- let convergenceRuns = 0;
246
- let hasChanges = true;
247
-
248
- while (hasChanges && convergenceRuns < 3) {
249
- convergenceRuns++;
250
-
251
- // Check for deletion
252
- const originalExists = await pathExists(
253
- path.join(testDir, "original.js")
254
- );
255
- const originalTracked = snapshot.files.has("original.js");
256
-
257
- if (!originalExists && originalTracked) {
258
- snapshotManager.removeFileEntry(snapshot, "original.js");
259
- }
260
-
261
- // Check for addition
262
- const newExists = await pathExists(path.join(testDir, "renamed.js"));
263
- const newTracked = snapshot.files.has("renamed.js");
264
-
265
- if (newExists && !newTracked) {
266
- snapshotManager.updateFileEntry(snapshot, "renamed.js", {
267
- path: path.join(testDir, "renamed.js"),
268
- url: "automerge:renamed" as any,
269
- head: ["renamed-head"] as any,
270
- extension: "js",
271
- mimeType: "text/javascript",
272
- });
273
- }
274
-
275
- // Determine if more runs needed
276
- // With the fix: Directory heads are properly updated, so convergence happens in 1 run
277
- if (convergenceRuns === 1) {
278
- hasChanges = false; // Fixed: converge immediately
279
- } else {
280
- // This shouldn't happen with the fix
281
- hasChanges = false;
282
- console.log(
283
- "🚨 UNEXPECTED: Required multiple runs - fix may not be working"
284
- );
285
- }
286
- }
287
-
288
- // Verify final state
289
- expect(await pathExists(path.join(testDir, "original.js"))).toBe(false);
290
- expect(await pathExists(path.join(testDir, "renamed.js"))).toBe(true);
291
- expect(snapshot.files.has("original.js")).toBe(false);
292
- expect(snapshot.files.has("renamed.js")).toBe(true);
293
-
294
- // Test assertion: Verify convergence in exactly 1 run
295
- expect(convergenceRuns).toBe(1);
296
- });
297
- });
298
- });