pushwork 2.0.0-a.sub.1 → 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 -157
  133. package/dist/core/sync-engine.d.ts.map +0 -1
  134. package/dist/core/sync-engine.js +0 -1379
  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 -1795
  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
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Black-box integration tests for branches, save (offline commit), status, and
3
+ * diff.
4
+ *
5
+ * `init` does a brief network roundtrip (waitForSync with a 3s floor on init),
6
+ * but everything else here is offline: save / status / diff / branch / switch /
7
+ * branches all use `repo.openRepo({ offline: true })` and never connect to a
8
+ * sync server.
9
+ */
10
+
11
+ import * as fs from "fs/promises";
12
+ import * as path from "path";
13
+ import * as tmp from "tmp";
14
+ import { execFile } from "child_process";
15
+ import { promisify } from "util";
16
+
17
+ const execFileP = promisify(execFile);
18
+ const CLI = path.join(__dirname, "..", "..", "dist", "cli.js");
19
+
20
+ const TEST_TIMEOUT = 60_000;
21
+
22
+ async function pushwork(
23
+ args: string[],
24
+ cwd?: string,
25
+ ): Promise<{ stdout: string; stderr: string }> {
26
+ try {
27
+ return await execFileP("node", [CLI, ...args], {
28
+ cwd,
29
+ env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
30
+ timeout: 45_000,
31
+ maxBuffer: 16 * 1024 * 1024,
32
+ });
33
+ } catch (err: any) {
34
+ const detail = [
35
+ `pushwork ${args.join(" ")} failed (cwd=${cwd ?? process.cwd()})`,
36
+ err.message,
37
+ err.stdout ? `stdout: ${err.stdout}` : "",
38
+ err.stderr ? `stderr: ${err.stderr}` : "",
39
+ ]
40
+ .filter(Boolean)
41
+ .join("\n");
42
+ throw new Error(detail);
43
+ }
44
+ }
45
+
46
+ async function pushworkExpectFail(
47
+ args: string[],
48
+ cwd?: string,
49
+ ): Promise<{ stdout: string; stderr: string; code: number }> {
50
+ try {
51
+ const ok = await execFileP("node", [CLI, ...args], {
52
+ cwd,
53
+ env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
54
+ timeout: 45_000,
55
+ maxBuffer: 16 * 1024 * 1024,
56
+ });
57
+ throw new Error(
58
+ `expected failure, got success: stdout=${ok.stdout} stderr=${ok.stderr}`,
59
+ );
60
+ } catch (err: any) {
61
+ if (typeof err.code !== "number") throw err;
62
+ return { stdout: err.stdout ?? "", stderr: err.stderr ?? "", code: err.code };
63
+ }
64
+ }
65
+
66
+ async function pathExists(p: string): Promise<boolean> {
67
+ try {
68
+ await fs.access(p);
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ async function readText(p: string): Promise<string> {
76
+ return fs.readFile(p, "utf8");
77
+ }
78
+
79
+ beforeAll(async () => {
80
+ await execFileP("pnpm", ["build"], {
81
+ cwd: path.join(__dirname, "..", ".."),
82
+ timeout: 120_000,
83
+ });
84
+ }, 120_000);
85
+
86
+ describe("pushwork branches & offline commands", () => {
87
+ let workRoot: string;
88
+ let cleanup: () => void;
89
+
90
+ beforeEach(() => {
91
+ const t = tmp.dirSync({ unsafeCleanup: true });
92
+ workRoot = t.name;
93
+ cleanup = t.removeCallback;
94
+ });
95
+
96
+ afterEach(() => cleanup());
97
+
98
+ async function initRepo(args: string[] = []): Promise<string> {
99
+ await fs.writeFile(path.join(workRoot, "a.txt"), "hello\n");
100
+ await pushwork(["init", "--sub", ...args], workRoot);
101
+ return workRoot;
102
+ }
103
+
104
+ describe("init defaults to branches mode", () => {
105
+ it(
106
+ "creates a .pushwork/branch file with 'default'",
107
+ async () => {
108
+ await initRepo();
109
+ expect(await readText(path.join(workRoot, ".pushwork", "branch"))).toBe(
110
+ "default\n",
111
+ );
112
+ },
113
+ TEST_TIMEOUT,
114
+ );
115
+
116
+ it(
117
+ "`pushwork branch` prints the current branch",
118
+ async () => {
119
+ await initRepo();
120
+ const { stdout } = await pushwork(["branch"], workRoot);
121
+ expect(stdout.trim()).toBe("default");
122
+ },
123
+ TEST_TIMEOUT,
124
+ );
125
+
126
+ it(
127
+ "`pushwork branches` lists the default branch and marks it current",
128
+ async () => {
129
+ await initRepo();
130
+ const { stdout } = await pushwork(["branches"], workRoot);
131
+ expect(stdout).toContain("* default");
132
+ },
133
+ TEST_TIMEOUT,
134
+ );
135
+
136
+ it(
137
+ "records branches=true in config.json",
138
+ async () => {
139
+ await initRepo();
140
+ const cfg = JSON.parse(
141
+ await readText(path.join(workRoot, ".pushwork", "config.json")),
142
+ );
143
+ expect(cfg.branches).toBe(true);
144
+ expect(cfg.version).toBe(3);
145
+ },
146
+ TEST_TIMEOUT,
147
+ );
148
+ });
149
+
150
+ describe("init --no-branches", () => {
151
+ it(
152
+ "records branches=false and creates no branch file",
153
+ async () => {
154
+ await initRepo(["--no-branches"]);
155
+ const cfg = JSON.parse(
156
+ await readText(path.join(workRoot, ".pushwork", "config.json")),
157
+ );
158
+ expect(cfg.branches).toBe(false);
159
+ expect(
160
+ await pathExists(path.join(workRoot, ".pushwork", "branch")),
161
+ ).toBe(false);
162
+ },
163
+ TEST_TIMEOUT,
164
+ );
165
+
166
+ it(
167
+ "`pushwork branch` prints (none) without args",
168
+ async () => {
169
+ await initRepo(["--no-branches"]);
170
+ const { stdout } = await pushwork(["branch"], workRoot);
171
+ expect(stdout.trim()).toBe("(none)");
172
+ },
173
+ TEST_TIMEOUT,
174
+ );
175
+
176
+ it(
177
+ "`pushwork branches` errors",
178
+ async () => {
179
+ await initRepo(["--no-branches"]);
180
+ const { stderr, code } = await pushworkExpectFail(
181
+ ["branches"],
182
+ workRoot,
183
+ );
184
+ expect(stderr).toContain("no branches");
185
+ expect(code).toBe(1);
186
+ },
187
+ TEST_TIMEOUT,
188
+ );
189
+ });
190
+
191
+ describe("status", () => {
192
+ it(
193
+ "is clean immediately after init",
194
+ async () => {
195
+ await initRepo();
196
+ const { stdout } = await pushwork(["status"], workRoot);
197
+ expect(stdout).toContain("On branch default");
198
+ expect(stdout).toContain("nothing to save");
199
+ },
200
+ TEST_TIMEOUT,
201
+ );
202
+
203
+ it(
204
+ "reports added/modified/deleted",
205
+ async () => {
206
+ await initRepo();
207
+ await fs.writeFile(path.join(workRoot, "a.txt"), "edited\n");
208
+ await fs.writeFile(path.join(workRoot, "added.txt"), "new\n");
209
+ await fs.writeFile(path.join(workRoot, "doomed.txt"), "delete me\n");
210
+ await pushwork(["save"], workRoot);
211
+ await fs.unlink(path.join(workRoot, "doomed.txt"));
212
+ await fs.writeFile(path.join(workRoot, "a.txt"), "edited again\n");
213
+ await fs.writeFile(path.join(workRoot, "another.txt"), "new2\n");
214
+
215
+ const { stdout } = await pushwork(["status"], workRoot);
216
+ expect(stdout).toContain("modified: a.txt");
217
+ expect(stdout).toContain("added: another.txt");
218
+ expect(stdout).toContain("deleted: doomed.txt");
219
+ },
220
+ TEST_TIMEOUT,
221
+ );
222
+ });
223
+
224
+ describe("save (offline commit)", () => {
225
+ it(
226
+ "clears status without contacting any sync server",
227
+ async () => {
228
+ await initRepo();
229
+ await fs.writeFile(path.join(workRoot, "b.txt"), "two\n");
230
+ const before = (await pushwork(["status"], workRoot)).stdout;
231
+ expect(before).toContain("added: b.txt");
232
+
233
+ // Point at deliberately unreachable endpoints so a network call
234
+ // would visibly fail. save must succeed regardless.
235
+ const env = {
236
+ ...process.env,
237
+ PUSHWORK_LEGACY_SERVER: "wss://127.0.0.1:1/never",
238
+ PUSHWORK_SUBDUCTION_SERVER: "wss://127.0.0.1:1/never",
239
+ FORCE_COLOR: "0",
240
+ NO_COLOR: "1",
241
+ };
242
+ await execFileP("node", [CLI, "save"], {
243
+ cwd: workRoot,
244
+ env,
245
+ timeout: 30_000,
246
+ });
247
+
248
+ const after = (await pushwork(["status"], workRoot)).stdout;
249
+ expect(after).toContain("nothing to save");
250
+ },
251
+ TEST_TIMEOUT,
252
+ );
253
+
254
+ it(
255
+ "`commit` is an alias for save",
256
+ async () => {
257
+ await initRepo();
258
+ await fs.writeFile(path.join(workRoot, "c.txt"), "c\n");
259
+ await pushwork(["commit"], workRoot);
260
+ const { stdout } = await pushwork(["status"], workRoot);
261
+ expect(stdout).toContain("nothing to save");
262
+ },
263
+ TEST_TIMEOUT,
264
+ );
265
+ });
266
+
267
+ describe("diff", () => {
268
+ it(
269
+ "shows a unified diff for modified files",
270
+ async () => {
271
+ await initRepo();
272
+ await fs.writeFile(path.join(workRoot, "a.txt"), "hello world\n");
273
+ const { stdout } = await pushwork(["diff"], workRoot);
274
+ expect(stdout).toContain("-hello");
275
+ expect(stdout).toContain("+hello world");
276
+ },
277
+ TEST_TIMEOUT,
278
+ );
279
+
280
+ it(
281
+ "prints (no changes) when clean",
282
+ async () => {
283
+ await initRepo();
284
+ const { stdout } = await pushwork(["diff"], workRoot);
285
+ expect(stdout.trim()).toBe("(no changes)");
286
+ },
287
+ TEST_TIMEOUT,
288
+ );
289
+ });
290
+
291
+ describe("branch / switch", () => {
292
+ it(
293
+ "branch <name> creates a new branch but doesn't switch",
294
+ async () => {
295
+ await initRepo();
296
+ await pushwork(["branch", "feat"], workRoot);
297
+ expect(
298
+ (await pushwork(["branch"], workRoot)).stdout.trim(),
299
+ ).toBe("default");
300
+ expect((await pushwork(["branches"], workRoot)).stdout).toContain(
301
+ "feat",
302
+ );
303
+ },
304
+ TEST_TIMEOUT,
305
+ );
306
+
307
+ it(
308
+ "switch <name> materializes the branch's tree",
309
+ async () => {
310
+ await initRepo();
311
+ await pushwork(["branch", "feat"], workRoot);
312
+
313
+ // Add a file on default and save
314
+ await fs.writeFile(path.join(workRoot, "default-only.txt"), "D\n");
315
+ await pushwork(["save"], workRoot);
316
+
317
+ // Switch to feat: default-only.txt should disappear (feat was branched
318
+ // from default before the save)
319
+ await pushwork(["switch", "feat"], workRoot);
320
+ expect(
321
+ await pathExists(path.join(workRoot, "default-only.txt")),
322
+ ).toBe(false);
323
+
324
+ // Switch back to default: file reappears
325
+ await pushwork(["switch", "default"], workRoot);
326
+ expect(
327
+ await readText(path.join(workRoot, "default-only.txt")),
328
+ ).toBe("D\n");
329
+ },
330
+ TEST_TIMEOUT,
331
+ );
332
+
333
+ it(
334
+ "switch refuses with uncommitted changes",
335
+ async () => {
336
+ await initRepo();
337
+ await pushwork(["branch", "feat"], workRoot);
338
+ await fs.writeFile(path.join(workRoot, "dirty.txt"), "uncommitted\n");
339
+
340
+ const { stderr, code } = await pushworkExpectFail(
341
+ ["switch", "feat"],
342
+ workRoot,
343
+ );
344
+ expect(code).toBe(1);
345
+ expect(stderr).toMatch(/uncommitted changes/);
346
+ },
347
+ TEST_TIMEOUT,
348
+ );
349
+
350
+ it(
351
+ "branch <name> errors when name already exists",
352
+ async () => {
353
+ await initRepo();
354
+ await pushwork(["branch", "feat"], workRoot);
355
+ const { stderr } = await pushworkExpectFail(
356
+ ["branch", "feat"],
357
+ workRoot,
358
+ );
359
+ expect(stderr).toContain("already exists");
360
+ },
361
+ TEST_TIMEOUT,
362
+ );
363
+
364
+ it(
365
+ "switch errors on a non-existent branch",
366
+ async () => {
367
+ await initRepo();
368
+ const { stderr } = await pushworkExpectFail(
369
+ ["switch", "nope"],
370
+ workRoot,
371
+ );
372
+ expect(stderr).toMatch(/does not exist/);
373
+ },
374
+ TEST_TIMEOUT,
375
+ );
376
+
377
+ it(
378
+ "switch with no name lists branches",
379
+ async () => {
380
+ await initRepo();
381
+ await pushwork(["branch", "feat"], workRoot);
382
+ const { stdout } = await pushwork(["switch"], workRoot);
383
+ expect(stdout).toContain("* default");
384
+ expect(stdout).toContain("feat");
385
+ },
386
+ TEST_TIMEOUT,
387
+ );
388
+ });
389
+ });