pushwork 2.0.0-preview → 2.0.0-preview.3

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 (169) hide show
  1. package/dist/branches.d.ts +1 -0
  2. package/dist/branches.d.ts.map +1 -1
  3. package/dist/cli/commands.d.ts +71 -0
  4. package/dist/cli/commands.d.ts.map +1 -0
  5. package/dist/cli/commands.js +794 -0
  6. package/dist/cli/commands.js.map +1 -0
  7. package/dist/cli/index.d.ts +2 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +19 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/cli.js +67 -112
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands.d.ts +58 -0
  14. package/dist/commands.d.ts.map +1 -0
  15. package/dist/commands.js +975 -0
  16. package/dist/commands.js.map +1 -0
  17. package/dist/config/index.d.ts +71 -0
  18. package/dist/config/index.d.ts.map +1 -0
  19. package/dist/config/index.js +314 -0
  20. package/dist/config/index.js.map +1 -0
  21. package/dist/config.d.ts +1 -2
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +1 -2
  24. package/dist/config.js.map +1 -1
  25. package/dist/core/change-detection.d.ts +80 -0
  26. package/dist/core/change-detection.d.ts.map +1 -0
  27. package/dist/core/change-detection.js +560 -0
  28. package/dist/core/change-detection.js.map +1 -0
  29. package/dist/core/config.d.ts +81 -0
  30. package/dist/core/config.d.ts.map +1 -0
  31. package/dist/core/config.js +304 -0
  32. package/dist/core/config.js.map +1 -0
  33. package/dist/core/index.d.ts +6 -0
  34. package/dist/core/index.d.ts.map +1 -0
  35. package/dist/core/index.js +22 -0
  36. package/dist/core/index.js.map +1 -0
  37. package/dist/core/move-detection.d.ts +34 -0
  38. package/dist/core/move-detection.d.ts.map +1 -0
  39. package/dist/core/move-detection.js +128 -0
  40. package/dist/core/move-detection.js.map +1 -0
  41. package/dist/core/snapshot.d.ts +105 -0
  42. package/dist/core/snapshot.d.ts.map +1 -0
  43. package/dist/core/snapshot.js +254 -0
  44. package/dist/core/snapshot.js.map +1 -0
  45. package/dist/core/sync-engine.d.ts +177 -0
  46. package/dist/core/sync-engine.d.ts.map +1 -0
  47. package/dist/core/sync-engine.js +1471 -0
  48. package/dist/core/sync-engine.js.map +1 -0
  49. package/dist/index.d.ts +2 -4
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +4 -14
  52. package/dist/index.js.map +1 -1
  53. package/dist/pushwork.d.ts +28 -61
  54. package/dist/pushwork.d.ts.map +1 -1
  55. package/dist/pushwork.js +127 -445
  56. package/dist/pushwork.js.map +1 -1
  57. package/dist/shapes/types.d.ts +1 -0
  58. package/dist/shapes/types.d.ts.map +1 -1
  59. package/dist/shapes/types.js.map +1 -1
  60. package/dist/shapes/vfs.d.ts.map +1 -1
  61. package/dist/shapes/vfs.js +6 -2
  62. package/dist/shapes/vfs.js.map +1 -1
  63. package/dist/snarf.d.ts +21 -0
  64. package/dist/snarf.d.ts.map +1 -0
  65. package/dist/snarf.js +117 -0
  66. package/dist/snarf.js.map +1 -0
  67. package/dist/stash.d.ts +0 -2
  68. package/dist/stash.d.ts.map +1 -1
  69. package/dist/stash.js +0 -1
  70. package/dist/stash.js.map +1 -1
  71. package/dist/types/config.d.ts +102 -0
  72. package/dist/types/config.d.ts.map +1 -0
  73. package/dist/types/config.js +10 -0
  74. package/dist/types/config.js.map +1 -0
  75. package/dist/types/documents.d.ts +88 -0
  76. package/dist/types/documents.d.ts.map +1 -0
  77. package/dist/types/documents.js +23 -0
  78. package/dist/types/documents.js.map +1 -0
  79. package/dist/types/index.d.ts +4 -0
  80. package/dist/types/index.d.ts.map +1 -0
  81. package/dist/types/index.js +20 -0
  82. package/dist/types/index.js.map +1 -0
  83. package/dist/types/snapshot.d.ts +64 -0
  84. package/dist/types/snapshot.d.ts.map +1 -0
  85. package/dist/types/snapshot.js +3 -0
  86. package/dist/types/snapshot.js.map +1 -0
  87. package/dist/utils/content-similarity.d.ts +53 -0
  88. package/dist/utils/content-similarity.d.ts.map +1 -0
  89. package/dist/utils/content-similarity.js +155 -0
  90. package/dist/utils/content-similarity.js.map +1 -0
  91. package/dist/utils/content.d.ts +10 -0
  92. package/dist/utils/content.d.ts.map +1 -0
  93. package/dist/utils/content.js +35 -0
  94. package/dist/utils/content.js.map +1 -0
  95. package/dist/utils/directory.d.ts +24 -0
  96. package/dist/utils/directory.d.ts.map +1 -0
  97. package/dist/utils/directory.js +56 -0
  98. package/dist/utils/directory.js.map +1 -0
  99. package/dist/utils/fs.d.ts +74 -0
  100. package/dist/utils/fs.d.ts.map +1 -0
  101. package/dist/utils/fs.js +298 -0
  102. package/dist/utils/fs.js.map +1 -0
  103. package/dist/utils/index.d.ts +5 -0
  104. package/dist/utils/index.d.ts.map +1 -0
  105. package/dist/utils/index.js +21 -0
  106. package/dist/utils/index.js.map +1 -0
  107. package/dist/utils/mime-types.d.ts +13 -0
  108. package/dist/utils/mime-types.d.ts.map +1 -0
  109. package/dist/utils/mime-types.js +247 -0
  110. package/dist/utils/mime-types.js.map +1 -0
  111. package/dist/utils/network-sync.d.ts +30 -0
  112. package/dist/utils/network-sync.d.ts.map +1 -0
  113. package/dist/utils/network-sync.js +391 -0
  114. package/dist/utils/network-sync.js.map +1 -0
  115. package/dist/utils/node-polyfills.d.ts +9 -0
  116. package/dist/utils/node-polyfills.d.ts.map +1 -0
  117. package/dist/utils/node-polyfills.js +9 -0
  118. package/dist/utils/node-polyfills.js.map +1 -0
  119. package/dist/utils/output.d.ts +129 -0
  120. package/dist/utils/output.d.ts.map +1 -0
  121. package/dist/utils/output.js +375 -0
  122. package/dist/utils/output.js.map +1 -0
  123. package/dist/utils/repo-factory.d.ts +15 -0
  124. package/dist/utils/repo-factory.d.ts.map +1 -0
  125. package/dist/utils/repo-factory.js +156 -0
  126. package/dist/utils/repo-factory.js.map +1 -0
  127. package/dist/utils/string-similarity.d.ts +14 -0
  128. package/dist/utils/string-similarity.d.ts.map +1 -0
  129. package/dist/utils/string-similarity.js +43 -0
  130. package/dist/utils/string-similarity.js.map +1 -0
  131. package/dist/utils/text-diff.d.ts +37 -0
  132. package/dist/utils/text-diff.d.ts.map +1 -0
  133. package/dist/utils/text-diff.js +131 -0
  134. package/dist/utils/text-diff.js.map +1 -0
  135. package/dist/utils/trace.d.ts +19 -0
  136. package/dist/utils/trace.d.ts.map +1 -0
  137. package/dist/utils/trace.js +68 -0
  138. package/dist/utils/trace.js.map +1 -0
  139. package/dist/version.d.ts +11 -0
  140. package/dist/version.d.ts.map +1 -0
  141. package/dist/version.js +93 -0
  142. package/dist/version.js.map +1 -0
  143. package/package.json +5 -1
  144. package/.prettierrc +0 -9
  145. package/flake.lock +0 -128
  146. package/flake.nix +0 -66
  147. package/pnpm-workspace.yaml +0 -5
  148. package/src/branches.ts +0 -93
  149. package/src/cli.ts +0 -292
  150. package/src/config.ts +0 -64
  151. package/src/fs-tree.ts +0 -70
  152. package/src/ignore.ts +0 -33
  153. package/src/index.ts +0 -38
  154. package/src/log.ts +0 -8
  155. package/src/pushwork.ts +0 -1055
  156. package/src/repo.ts +0 -76
  157. package/src/shapes/custom.ts +0 -29
  158. package/src/shapes/file.ts +0 -115
  159. package/src/shapes/index.ts +0 -19
  160. package/src/shapes/patchwork-folder.ts +0 -156
  161. package/src/shapes/types.ts +0 -79
  162. package/src/shapes/vfs.ts +0 -93
  163. package/src/stash.ts +0 -106
  164. package/test/integration/branches.test.ts +0 -389
  165. package/test/integration/pushwork.test.ts +0 -547
  166. package/test/setup.ts +0 -29
  167. package/test/unit/doc-shape.test.ts +0 -612
  168. package/tsconfig.json +0 -22
  169. package/vitest.config.ts +0 -14
@@ -1,547 +0,0 @@
1
- /**
2
- * Integration tests for pushwork.
3
- *
4
- * Black-box: drive the CLI as a subprocess, observe filesystem and stdout.
5
- * No imports from src/ — these tests are the spec for what pushwork does,
6
- * not how it does it.
7
- *
8
- * Run against both supported sync backends:
9
- * - "legacy" → default WebSocket sync server (no flag)
10
- * - "subduction" → --sub flag on init/clone (persisted in config; used
11
- * automatically by subsequent sync runs)
12
- *
13
- * Tests hit the public sync servers, so each test allows generous time
14
- * for network roundtrips.
15
- */
16
-
17
- import * as fs from "fs/promises";
18
- import * as path from "path";
19
- import * as crypto from "crypto";
20
- import * as tmp from "tmp";
21
- import { execFile } from "child_process";
22
- import { promisify } from "util";
23
-
24
- const execFileP = promisify(execFile);
25
- const CLI = path.join(__dirname, "..", "..", "dist", "cli.js");
26
-
27
- const TEST_TIMEOUT = 120_000;
28
-
29
- type Backend = { name: string; flags: string[] };
30
- const BACKENDS: Backend[] = [
31
- { name: "legacy", flags: [] },
32
- { name: "subduction", flags: ["--sub"] },
33
- ];
34
-
35
- async function pushwork(
36
- args: string[],
37
- cwd?: string,
38
- ): Promise<{ stdout: string; stderr: string }> {
39
- try {
40
- return await execFileP("node", [CLI, ...args], {
41
- cwd,
42
- env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
43
- timeout: 90_000,
44
- maxBuffer: 16 * 1024 * 1024,
45
- });
46
- } catch (err: any) {
47
- const detail = [
48
- `pushwork ${args.join(" ")} failed (cwd=${cwd ?? process.cwd()})`,
49
- err.message,
50
- err.stdout ? `stdout: ${err.stdout}` : "",
51
- err.stderr ? `stderr: ${err.stderr}` : "",
52
- ]
53
- .filter(Boolean)
54
- .join("\n");
55
- throw new Error(detail);
56
- }
57
- }
58
-
59
- async function pathExists(p: string): Promise<boolean> {
60
- try {
61
- await fs.access(p);
62
- return true;
63
- } catch {
64
- return false;
65
- }
66
- }
67
-
68
- async function readText(p: string): Promise<string> {
69
- return fs.readFile(p, "utf8");
70
- }
71
-
72
- async function listUserFiles(dir: string): Promise<string[]> {
73
- const out: string[] = [];
74
- async function walk(current: string) {
75
- const entries = await fs.readdir(current, { withFileTypes: true });
76
- for (const entry of entries) {
77
- if (entry.name === ".pushwork") continue;
78
- const full = path.join(current, entry.name);
79
- if (entry.isDirectory()) {
80
- await walk(full);
81
- } else if (entry.isFile()) {
82
- out.push(path.relative(dir, full));
83
- }
84
- }
85
- }
86
- await walk(dir);
87
- return out.sort();
88
- }
89
-
90
- async function hashUserContent(dir: string): Promise<string> {
91
- const files = await listUserFiles(dir);
92
- const h = crypto.createHash("sha256");
93
- for (const f of files) {
94
- h.update(f);
95
- h.update("\0");
96
- h.update(await fs.readFile(path.join(dir, f)));
97
- h.update("\0");
98
- }
99
- return h.digest("hex");
100
- }
101
-
102
- async function syncOnce(repos: string[]): Promise<void> {
103
- for (const r of repos) await pushwork(["sync"], r);
104
- }
105
-
106
- async function syncUntilConverged(
107
- repos: string[],
108
- maxRounds = 6,
109
- ): Promise<number> {
110
- for (let round = 1; round <= maxRounds; round++) {
111
- await syncOnce(repos);
112
- const hashes = await Promise.all(repos.map(hashUserContent));
113
- if (hashes.every((h) => h === hashes[0])) return round;
114
- }
115
- const hashes = await Promise.all(repos.map(hashUserContent));
116
- const debug = await Promise.all(
117
- repos.map(async (r, i) => `${i}: ${(await listUserFiles(r)).join(",")}`),
118
- );
119
- throw new Error(
120
- `failed to converge after ${maxRounds} rounds:\n hashes: ${hashes
121
- .map((h) => h.slice(0, 12))
122
- .join(" / ")}\n files:\n ${debug.join("\n ")}`,
123
- );
124
- }
125
-
126
- beforeAll(async () => {
127
- // Build once for the entire suite.
128
- await execFileP("pnpm", ["build"], {
129
- cwd: path.join(__dirname, "..", ".."),
130
- timeout: 120_000,
131
- });
132
- }, 120_000);
133
-
134
- describe.each(BACKENDS)("pushwork — $name backend", ({ flags }) => {
135
- let workRoot: string;
136
- let cleanup: () => void;
137
-
138
- beforeEach(() => {
139
- const t = tmp.dirSync({ unsafeCleanup: true });
140
- workRoot = t.name;
141
- cleanup = t.removeCallback;
142
- });
143
-
144
- afterEach(() => cleanup());
145
-
146
- describe("init", () => {
147
- it(
148
- "succeeds on an empty directory",
149
- async () => {
150
- await pushwork(["init", ...flags, workRoot]);
151
- expect(await listUserFiles(workRoot)).toEqual([]);
152
- },
153
- TEST_TIMEOUT,
154
- );
155
-
156
- it(
157
- "succeeds on a directory containing files",
158
- async () => {
159
- await fs.writeFile(path.join(workRoot, "a.txt"), "hello");
160
- await fs.writeFile(path.join(workRoot, "b.md"), "# B");
161
- await pushwork(["init", ...flags, workRoot]);
162
- },
163
- TEST_TIMEOUT,
164
- );
165
-
166
- it(
167
- "does not destroy or alter pre-existing user files",
168
- async () => {
169
- await fs.writeFile(path.join(workRoot, "keep.txt"), "do not touch");
170
- await fs.mkdir(path.join(workRoot, "subdir"));
171
- await fs.writeFile(
172
- path.join(workRoot, "subdir", "nested.txt"),
173
- "nested",
174
- );
175
-
176
- await pushwork(["init", ...flags, workRoot]);
177
-
178
- expect(await readText(path.join(workRoot, "keep.txt"))).toBe(
179
- "do not touch",
180
- );
181
- expect(
182
- await readText(path.join(workRoot, "subdir", "nested.txt")),
183
- ).toBe("nested");
184
- },
185
- TEST_TIMEOUT,
186
- );
187
- });
188
-
189
- describe("url", () => {
190
- it(
191
- "prints an automerge: URL after init",
192
- async () => {
193
- await pushwork(["init", ...flags, workRoot]);
194
- const { stdout } = await pushwork(["url"], workRoot);
195
- expect(stdout.trim()).toMatch(/^automerge:[A-Za-z0-9]+/);
196
- },
197
- TEST_TIMEOUT,
198
- );
199
-
200
- it(
201
- "is stable across calls within one repo",
202
- async () => {
203
- await pushwork(["init", ...flags, workRoot]);
204
- const a = (await pushwork(["url"], workRoot)).stdout.trim();
205
- const b = (await pushwork(["url"], workRoot)).stdout.trim();
206
- expect(a).toBe(b);
207
- },
208
- TEST_TIMEOUT,
209
- );
210
-
211
- it(
212
- "differs between two independently initialized repos",
213
- async () => {
214
- const r1 = path.join(workRoot, "r1");
215
- const r2 = path.join(workRoot, "r2");
216
- await fs.mkdir(r1);
217
- await fs.mkdir(r2);
218
- await pushwork(["init", ...flags], r1);
219
- await pushwork(["init", ...flags], r2);
220
- const u1 = (await pushwork(["url"], r1)).stdout.trim();
221
- const u2 = (await pushwork(["url"], r2)).stdout.trim();
222
- expect(u1).not.toBe(u2);
223
- },
224
- TEST_TIMEOUT,
225
- );
226
- });
227
-
228
- describe("clone", () => {
229
- it(
230
- "reproduces a single text file",
231
- async () => {
232
- const a = path.join(workRoot, "a");
233
- const b = path.join(workRoot, "b");
234
- await fs.mkdir(a);
235
-
236
- await fs.writeFile(path.join(a, "hello.txt"), "Hello, World!");
237
- await pushwork(["init", ...flags], a);
238
-
239
- const url = (await pushwork(["url"], a)).stdout.trim();
240
- await pushwork(["clone", ...flags, url, b]);
241
-
242
- expect(await readText(path.join(b, "hello.txt"))).toBe("Hello, World!");
243
- },
244
- TEST_TIMEOUT,
245
- );
246
-
247
- it(
248
- "reproduces a nested directory tree",
249
- async () => {
250
- const a = path.join(workRoot, "a");
251
- const b = path.join(workRoot, "b");
252
- await fs.mkdir(a);
253
- await fs.mkdir(path.join(a, "src", "components"), {
254
- recursive: true,
255
- });
256
- await fs.writeFile(path.join(a, "package.json"), '{"name":"x"}');
257
- await fs.writeFile(path.join(a, "src", "index.ts"), "export {}");
258
- await fs.writeFile(
259
- path.join(a, "src", "components", "Button.tsx"),
260
- "export const Button = () => null",
261
- );
262
-
263
- await pushwork(["init", ...flags], a);
264
- const url = (await pushwork(["url"], a)).stdout.trim();
265
- await pushwork(["clone", ...flags, url, b]);
266
-
267
- expect(await readText(path.join(b, "package.json"))).toBe(
268
- '{"name":"x"}',
269
- );
270
- expect(await readText(path.join(b, "src", "index.ts"))).toBe(
271
- "export {}",
272
- );
273
- expect(
274
- await readText(path.join(b, "src", "components", "Button.tsx")),
275
- ).toBe("export const Button = () => null");
276
- },
277
- TEST_TIMEOUT,
278
- );
279
-
280
- it(
281
- "reproduces binary file content byte-for-byte",
282
- async () => {
283
- const a = path.join(workRoot, "a");
284
- const b = path.join(workRoot, "b");
285
- await fs.mkdir(a);
286
-
287
- const bytes = Buffer.from([
288
- 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x01, 0x02,
289
- 0xff, 0xfe, 0x10, 0x42,
290
- ]);
291
- await fs.writeFile(path.join(a, "image.png"), bytes);
292
-
293
- await pushwork(["init", ...flags], a);
294
- const url = (await pushwork(["url"], a)).stdout.trim();
295
- await pushwork(["clone", ...flags, url, b]);
296
-
297
- const out = await fs.readFile(path.join(b, "image.png"));
298
- expect(out.equals(bytes)).toBe(true);
299
- },
300
- TEST_TIMEOUT,
301
- );
302
-
303
- it(
304
- "a fresh clone reports the same URL as the source",
305
- async () => {
306
- const a = path.join(workRoot, "a");
307
- const b = path.join(workRoot, "b");
308
- await fs.mkdir(a);
309
- await fs.writeFile(path.join(a, "x.txt"), "x");
310
-
311
- await pushwork(["init", ...flags], a);
312
- const urlA = (await pushwork(["url"], a)).stdout.trim();
313
- await pushwork(["clone", ...flags, urlA, b]);
314
-
315
- const urlB = (await pushwork(["url"], b)).stdout.trim();
316
- expect(urlB).toBe(urlA);
317
- },
318
- TEST_TIMEOUT,
319
- );
320
- });
321
-
322
- describe("sync — propagation between two repos", () => {
323
- async function setupPair() {
324
- const a = path.join(workRoot, "a");
325
- const b = path.join(workRoot, "b");
326
- await fs.mkdir(a);
327
- await pushwork(["init", ...flags], a);
328
- const url = (await pushwork(["url"], a)).stdout.trim();
329
- await pushwork(["clone", ...flags, url, b]);
330
- return { a, b };
331
- }
332
-
333
- it(
334
- "propagates a new file from A to B",
335
- async () => {
336
- const { a, b } = await setupPair();
337
-
338
- await fs.writeFile(path.join(a, "added.txt"), "new in A");
339
- await syncUntilConverged([a, b]);
340
-
341
- expect(await readText(path.join(b, "added.txt"))).toBe("new in A");
342
- },
343
- TEST_TIMEOUT,
344
- );
345
-
346
- it(
347
- "propagates a new file from B to A",
348
- async () => {
349
- const { a, b } = await setupPair();
350
-
351
- await fs.writeFile(path.join(b, "from-b.txt"), "new in B");
352
- await syncUntilConverged([a, b]);
353
-
354
- expect(await readText(path.join(a, "from-b.txt"))).toBe("new in B");
355
- },
356
- TEST_TIMEOUT,
357
- );
358
-
359
- it(
360
- "propagates a modification",
361
- async () => {
362
- const { a, b } = await setupPair();
363
-
364
- await fs.writeFile(path.join(a, "x.txt"), "v1");
365
- await syncUntilConverged([a, b]);
366
- expect(await readText(path.join(b, "x.txt"))).toBe("v1");
367
-
368
- await fs.writeFile(path.join(a, "x.txt"), "v2");
369
- await syncUntilConverged([a, b]);
370
- expect(await readText(path.join(b, "x.txt"))).toBe("v2");
371
- },
372
- TEST_TIMEOUT,
373
- );
374
-
375
- it(
376
- "propagates a deletion",
377
- async () => {
378
- const { a, b } = await setupPair();
379
-
380
- await fs.writeFile(path.join(a, "doomed.txt"), "doomed");
381
- await fs.writeFile(path.join(a, "kept.txt"), "kept");
382
- await syncUntilConverged([a, b]);
383
- expect(await pathExists(path.join(b, "doomed.txt"))).toBe(true);
384
-
385
- await fs.unlink(path.join(a, "doomed.txt"));
386
- await syncUntilConverged([a, b]);
387
-
388
- expect(await pathExists(path.join(b, "doomed.txt"))).toBe(false);
389
- expect(await readText(path.join(b, "kept.txt"))).toBe("kept");
390
- },
391
- TEST_TIMEOUT,
392
- );
393
-
394
- it(
395
- "propagates changes inside a nested directory",
396
- async () => {
397
- const { a, b } = await setupPair();
398
-
399
- await fs.mkdir(path.join(a, "deep", "deeper"), { recursive: true });
400
- await fs.writeFile(
401
- path.join(a, "deep", "deeper", "leaf.txt"),
402
- "leaf v1",
403
- );
404
- await syncUntilConverged([a, b]);
405
-
406
- expect(
407
- await readText(path.join(b, "deep", "deeper", "leaf.txt")),
408
- ).toBe("leaf v1");
409
-
410
- await fs.writeFile(
411
- path.join(a, "deep", "deeper", "leaf.txt"),
412
- "leaf v2",
413
- );
414
- await syncUntilConverged([a, b]);
415
-
416
- expect(
417
- await readText(path.join(b, "deep", "deeper", "leaf.txt")),
418
- ).toBe("leaf v2");
419
- },
420
- TEST_TIMEOUT,
421
- );
422
-
423
- it(
424
- "converges concurrent disjoint edits",
425
- async () => {
426
- const { a, b } = await setupPair();
427
-
428
- await fs.writeFile(path.join(a, "from-a.txt"), "A");
429
- await fs.writeFile(path.join(b, "from-b.txt"), "B");
430
-
431
- await syncUntilConverged([a, b]);
432
-
433
- expect(await readText(path.join(a, "from-a.txt"))).toBe("A");
434
- expect(await readText(path.join(a, "from-b.txt"))).toBe("B");
435
- expect(await readText(path.join(b, "from-a.txt"))).toBe("A");
436
- expect(await readText(path.join(b, "from-b.txt"))).toBe("B");
437
- },
438
- TEST_TIMEOUT,
439
- );
440
-
441
- it(
442
- "a third clone catches up to the current state",
443
- async () => {
444
- const { a, b } = await setupPair();
445
-
446
- await fs.writeFile(path.join(a, "shared.txt"), "shared content");
447
- await syncUntilConverged([a, b]);
448
-
449
- const c = path.join(workRoot, "c");
450
- const url = (await pushwork(["url"], a)).stdout.trim();
451
- await pushwork(["clone", ...flags, url, c]);
452
-
453
- expect(await readText(path.join(c, "shared.txt"))).toBe(
454
- "shared content",
455
- );
456
- },
457
- TEST_TIMEOUT,
458
- );
459
- });
460
-
461
- describe("default exclusions", () => {
462
- it(
463
- "does not sync .git or node_modules to a clone",
464
- async () => {
465
- const a = path.join(workRoot, "a");
466
- const b = path.join(workRoot, "b");
467
- await fs.mkdir(a);
468
-
469
- await fs.writeFile(path.join(a, "ok.txt"), "ok");
470
-
471
- await fs.mkdir(path.join(a, "node_modules"));
472
- await fs.writeFile(path.join(a, "node_modules", "lib.js"), "lib");
473
-
474
- await fs.mkdir(path.join(a, ".git"));
475
- await fs.writeFile(path.join(a, ".git", "HEAD"), "ref");
476
-
477
- await pushwork(["init", ...flags], a);
478
- const url = (await pushwork(["url"], a)).stdout.trim();
479
- await pushwork(["clone", ...flags, url, b]);
480
-
481
- expect(await pathExists(path.join(b, "ok.txt"))).toBe(true);
482
- expect(await pathExists(path.join(b, "node_modules"))).toBe(false);
483
- expect(await pathExists(path.join(b, ".git"))).toBe(false);
484
- },
485
- TEST_TIMEOUT,
486
- );
487
- });
488
-
489
- describe("end-to-end session", () => {
490
- it(
491
- "supports a realistic two-user collaboration session",
492
- async () => {
493
- // Alice initializes a project with several files.
494
- const alice = path.join(workRoot, "alice");
495
- await fs.mkdir(alice);
496
- await fs.writeFile(path.join(alice, "README"), "# Project");
497
- await fs.mkdir(path.join(alice, "src"));
498
- await fs.writeFile(path.join(alice, "src", "main.ts"), "// v1");
499
- await pushwork(["init", ...flags], alice);
500
-
501
- // Bob clones Alice's project.
502
- const bob = path.join(workRoot, "bob");
503
- const url = (await pushwork(["url"], alice)).stdout.trim();
504
- await pushwork(["clone", ...flags, url, bob]);
505
-
506
- expect(await readText(path.join(bob, "README"))).toBe("# Project");
507
- expect(await readText(path.join(bob, "src", "main.ts"))).toBe("// v1");
508
-
509
- // Bob edits a file and adds a new one. Alice edits a different file.
510
- await fs.writeFile(path.join(bob, "src", "main.ts"), "// v2 (bob)");
511
- await fs.writeFile(
512
- path.join(bob, "src", "util.ts"),
513
- "export const x = 1",
514
- );
515
- await fs.writeFile(path.join(alice, "README"), "# Project\n\nNotes");
516
-
517
- await syncUntilConverged([alice, bob]);
518
-
519
- // Both should see all changes.
520
- expect(await readText(path.join(alice, "src", "main.ts"))).toBe(
521
- "// v2 (bob)",
522
- );
523
- expect(await readText(path.join(alice, "src", "util.ts"))).toBe(
524
- "export const x = 1",
525
- );
526
- expect(await readText(path.join(bob, "README"))).toBe(
527
- "# Project\n\nNotes",
528
- );
529
-
530
- // Bob deletes the util file.
531
- await fs.unlink(path.join(bob, "src", "util.ts"));
532
- await syncUntilConverged([alice, bob]);
533
-
534
- expect(await pathExists(path.join(alice, "src", "util.ts"))).toBe(
535
- false,
536
- );
537
- expect(await pathExists(path.join(bob, "src", "util.ts"))).toBe(false);
538
-
539
- // Final state must be byte-for-byte identical.
540
- expect(await hashUserContent(alice)).toBe(
541
- await hashUserContent(bob),
542
- );
543
- },
544
- TEST_TIMEOUT * 2,
545
- );
546
- });
547
- });
package/test/setup.ts DELETED
@@ -1,29 +0,0 @@
1
- /**
2
- * Vitest setup: stub ESM-only modules that misbehave under bundled tests.
3
- */
4
- import { vi } from "vitest";
5
-
6
- vi.mock("chalk", () => ({
7
- default: new Proxy(
8
- {},
9
- {
10
- get: (target, prop) => {
11
- if (prop === "default") return target;
12
- return (str: string) => str;
13
- },
14
- },
15
- ),
16
- }));
17
-
18
- vi.mock("ora", () => ({
19
- default: vi.fn(() => ({
20
- start: vi.fn().mockReturnThis(),
21
- stop: vi.fn().mockReturnThis(),
22
- succeed: vi.fn().mockReturnThis(),
23
- fail: vi.fn().mockReturnThis(),
24
- warn: vi.fn().mockReturnThis(),
25
- info: vi.fn().mockReturnThis(),
26
- clear: vi.fn().mockReturnThis(),
27
- text: "",
28
- })),
29
- }));