@ibgib/vcs-gib 0.0.46 → 0.0.49

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.
@@ -0,0 +1,366 @@
1
+ /**
2
+ * @module delete-files.respec
3
+ *
4
+ * Unit tests for VCS file and folder deletion workflows:
5
+ * - vcs add <deleted-file>
6
+ * - vcs add -a (auto-staging missing files as deleted)
7
+ * - vcs status (rendering staged & unstaged deletions)
8
+ * - vcs commit (Merkle DAG negative-space pruning)
9
+ * - vcs commit --keep-empty-folders vs --rm-empty-folders
10
+ * - vcs restore <deleted-file> (recovering deleted file to worktree)
11
+ * - vcs reset <deleted-file> (unstaging deletion)
12
+ */
13
+
14
+ import path from 'node:path';
15
+ import fs from 'node:fs/promises';
16
+
17
+ import {
18
+ respecfully, ifWe, iReckon, ifWeMight,
19
+ } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
20
+
21
+ import { executeInitCmd } from './cli-init-cmd.mjs';
22
+ import { executeAddCmd } from './cli-add-cmd.mjs';
23
+ import { executeCommitCmd } from './cli-commit-cmd.mjs';
24
+ import { executeStatusCmd } from './cli-status-cmd.mjs';
25
+ import { executeResetCmd } from './cli-reset-cmd.mjs';
26
+ import { executeRestoreCmd } from './cli-restore-cmd.mjs';
27
+
28
+ const lc = `[apps/vcs-gib/src/cli/commands/delete-files.respec.mts]`;
29
+
30
+ await respecfully(lc, `VCS Deletions Lifecycle Command Handlers`, async () => {
31
+
32
+ await ifWe(lc, `vcs add <deleted-file> stages deletion, and vcs commit prunes it from Merkle tree`, async () => {
33
+ const testDir = path.join(process.cwd(), '.test_delete_single');
34
+ const vcsFolderName = '.vcsgib_test_delete_single';
35
+ try {
36
+ await executeInitCmd({
37
+ targetDir: testDir,
38
+ vcsFolderName,
39
+ quiet: true,
40
+ });
41
+
42
+ await fs.writeFile(path.join(testDir, 'file1.txt'), 'Content 1', 'utf8');
43
+
44
+ await executeAddCmd({
45
+ paths: ['file1.txt'],
46
+ targetDir: testDir,
47
+ vcsFolderName,
48
+ quiet: true,
49
+ });
50
+
51
+ await executeCommitCmd({
52
+ message: 'Add file1.txt',
53
+ targetDir: testDir,
54
+ vcsFolderName,
55
+ quiet: true,
56
+ });
57
+
58
+ // Delete file on disk
59
+ await fs.unlink(path.join(testDir, 'file1.txt'));
60
+
61
+ // Status shows unstaged deletion
62
+ let statusRes = await executeStatusCmd({
63
+ targetDir: testDir,
64
+ vcsFolderName,
65
+ quiet: true,
66
+ });
67
+ iReckon(lc, statusRes.deleted.map(x => x.path)).includes('file1.txt');
68
+
69
+ // Stage deletion via vcs add <path>
70
+ const addRes = await executeAddCmd({
71
+ paths: ['file1.txt'],
72
+ targetDir: testDir,
73
+ vcsFolderName,
74
+ quiet: true,
75
+ });
76
+ iReckon(lc, addRes.stagedPaths).includes('file1.txt');
77
+
78
+ // Status shows staged deletion
79
+ statusRes = await executeStatusCmd({
80
+ targetDir: testDir,
81
+ vcsFolderName,
82
+ quiet: true,
83
+ });
84
+ iReckon(lc, statusRes.staged.map(x => x.path)).includes('file1.txt');
85
+ const stagedItem = statusRes.staged.find(x => x.path === 'file1.txt');
86
+ iReckon(lc, stagedItem?.stagedType).willEqual('deleted');
87
+
88
+ // Commit deletion
89
+ const commitRes = await executeCommitCmd({
90
+ message: 'Delete file1.txt',
91
+ targetDir: testDir,
92
+ vcsFolderName,
93
+ quiet: true,
94
+ });
95
+ iReckon(lc, commitRes.committedPaths).includes('file1.txt');
96
+
97
+ // Status is clean after commit
98
+ statusRes = await executeStatusCmd({
99
+ targetDir: testDir,
100
+ vcsFolderName,
101
+ quiet: true,
102
+ });
103
+ iReckon(lc, statusRes.staged.length).willEqual(0);
104
+ iReckon(lc, statusRes.deleted.length).willEqual(0);
105
+ iReckon(lc, statusRes.modified.length).willEqual(0);
106
+ } finally {
107
+ await fs.rm(testDir, { recursive: true, force: true });
108
+ }
109
+ });
110
+
111
+ await ifWe(lc, `vcs add -a stages multiple deleted files and folder pruning occurs on commit`, async () => {
112
+ const testDir = path.join(process.cwd(), '.test_delete_all');
113
+ const vcsFolderName = '.vcsgib_test_delete_all';
114
+ try {
115
+ await executeInitCmd({
116
+ targetDir: testDir,
117
+ vcsFolderName,
118
+ quiet: true,
119
+ });
120
+
121
+ await fs.mkdir(path.join(testDir, 'src', 'utils'), { recursive: true });
122
+ await fs.writeFile(path.join(testDir, 'src', 'utils', 'math.ts'), 'export const pi = 3.14;', 'utf8');
123
+ await fs.writeFile(path.join(testDir, 'src', 'index.ts'), 'export * from "./utils/math.js";', 'utf8');
124
+ await fs.writeFile(path.join(testDir, 'readme.md'), '# Test Project', 'utf8');
125
+
126
+ await executeAddCmd({
127
+ all: true,
128
+ targetDir: testDir,
129
+ vcsFolderName,
130
+ quiet: true,
131
+ });
132
+
133
+ await executeCommitCmd({
134
+ message: 'Initial project files',
135
+ targetDir: testDir,
136
+ vcsFolderName,
137
+ quiet: true,
138
+ });
139
+
140
+ // Delete math.ts
141
+ await fs.unlink(path.join(testDir, 'src', 'utils', 'math.ts'));
142
+
143
+ // vcs add -a auto-stages the deleted file
144
+ const addRes = await executeAddCmd({
145
+ all: true,
146
+ targetDir: testDir,
147
+ vcsFolderName,
148
+ quiet: true,
149
+ });
150
+ iReckon(lc, addRes.stagedPaths).includes('src/utils/math.ts');
151
+
152
+ // Status shows staged deletion
153
+ let statusRes = await executeStatusCmd({
154
+ targetDir: testDir,
155
+ vcsFolderName,
156
+ quiet: true,
157
+ });
158
+ iReckon(lc, statusRes.staged.map(x => x.path)).includes('src/utils/math.ts');
159
+
160
+ // Commit with default pruning (--rm-empty-folders)
161
+ const commitRes = await executeCommitCmd({
162
+ message: 'Remove math utility',
163
+ targetDir: testDir,
164
+ vcsFolderName,
165
+ quiet: true,
166
+ });
167
+ iReckon(lc, commitRes.committedPaths).includes('src/utils/math.ts');
168
+
169
+ statusRes = await executeStatusCmd({
170
+ targetDir: testDir,
171
+ vcsFolderName,
172
+ quiet: true,
173
+ });
174
+ iReckon(lc, statusRes.staged.length).willEqual(0);
175
+ iReckon(lc, statusRes.deleted.length).willEqual(0);
176
+ } finally {
177
+ await fs.rm(testDir, { recursive: true, force: true });
178
+ }
179
+ });
180
+
181
+ await ifWe(lc, `vcs commit with keepEmptyFolders retains empty folder node in tree`, async () => {
182
+ const testDir = path.join(process.cwd(), '.test_delete_keep_empty');
183
+ const vcsFolderName = '.vcsgib_test_delete_keep_empty';
184
+ try {
185
+ await executeInitCmd({
186
+ targetDir: testDir,
187
+ vcsFolderName,
188
+ quiet: true,
189
+ });
190
+
191
+ await fs.mkdir(path.join(testDir, 'emptyDir'), { recursive: true });
192
+ await fs.writeFile(path.join(testDir, 'emptyDir', 'temp.txt'), 'temporary', 'utf8');
193
+
194
+ await executeAddCmd({
195
+ all: true,
196
+ targetDir: testDir,
197
+ vcsFolderName,
198
+ quiet: true,
199
+ });
200
+
201
+ await executeCommitCmd({
202
+ message: 'Initial with temp.txt',
203
+ targetDir: testDir,
204
+ vcsFolderName,
205
+ quiet: true,
206
+ });
207
+
208
+ // Delete temp.txt
209
+ await fs.unlink(path.join(testDir, 'emptyDir', 'temp.txt'));
210
+
211
+ await executeAddCmd({
212
+ paths: ['emptyDir/temp.txt'],
213
+ targetDir: testDir,
214
+ vcsFolderName,
215
+ quiet: true,
216
+ });
217
+
218
+ // Commit with keepEmptyFolders: true
219
+ const commitRes = await executeCommitCmd({
220
+ message: 'Delete temp keeping empty dir',
221
+ targetDir: testDir,
222
+ vcsFolderName,
223
+ quiet: true,
224
+ keepEmptyFolders: true,
225
+ });
226
+ iReckon(lc, commitRes.committedPaths).includes('emptyDir/temp.txt');
227
+
228
+ const statusRes = await executeStatusCmd({
229
+ targetDir: testDir,
230
+ vcsFolderName,
231
+ quiet: true,
232
+ });
233
+ iReckon(lc, statusRes.staged.length).willEqual(0);
234
+ } finally {
235
+ await fs.rm(testDir, { recursive: true, force: true });
236
+ }
237
+ });
238
+
239
+ await ifWe(lc, `vcs restore <file> restores deleted file from committed snapshot to disk`, async () => {
240
+ const testDir = path.join(process.cwd(), '.test_restore_deleted');
241
+ const vcsFolderName = '.vcsgib_test_restore_deleted';
242
+ try {
243
+ await executeInitCmd({
244
+ targetDir: testDir,
245
+ vcsFolderName,
246
+ quiet: true,
247
+ });
248
+
249
+ await fs.mkdir(path.join(testDir, 'assets', 'css'), { recursive: true });
250
+ await fs.writeFile(path.join(testDir, 'assets', 'css', 'style.css'), 'body { margin: 0; }', 'utf8');
251
+
252
+ await executeAddCmd({
253
+ all: true,
254
+ targetDir: testDir,
255
+ vcsFolderName,
256
+ quiet: true,
257
+ });
258
+
259
+ await executeCommitCmd({
260
+ message: 'Add stylesheet',
261
+ targetDir: testDir,
262
+ vcsFolderName,
263
+ quiet: true,
264
+ });
265
+
266
+ // Delete style.css from disk
267
+ await fs.unlink(path.join(testDir, 'assets', 'css', 'style.css'));
268
+
269
+ let statusRes = await executeStatusCmd({
270
+ targetDir: testDir,
271
+ vcsFolderName,
272
+ quiet: true,
273
+ });
274
+ iReckon(lc, statusRes.deleted.map(x => x.path)).includes('assets/css/style.css');
275
+
276
+ // Restore deleted file from committed snapshot
277
+ const restoreRes = await executeRestoreCmd({
278
+ paths: ['assets/css/style.css'],
279
+ targetDir: testDir,
280
+ vcsFolderName,
281
+ quiet: true,
282
+ });
283
+ iReckon(lc, restoreRes.restoredPaths).includes('assets/css/style.css');
284
+
285
+ // Verify file is back on disk at correct nested path with correct content
286
+ const restoredContent = await fs.readFile(path.join(testDir, 'assets', 'css', 'style.css'), 'utf8');
287
+ iReckon(lc, restoredContent).willEqual('body { margin: 0; }');
288
+
289
+ statusRes = await executeStatusCmd({
290
+ targetDir: testDir,
291
+ vcsFolderName,
292
+ quiet: true,
293
+ });
294
+ iReckon(lc, statusRes.deleted.length).willEqual(0);
295
+ } finally {
296
+ await fs.rm(testDir, { recursive: true, force: true });
297
+ }
298
+ });
299
+
300
+ await ifWe(lc, `vcs reset <file> unstages a staged deletion`, async () => {
301
+ const testDir = path.join(process.cwd(), '.test_reset_deletion');
302
+ const vcsFolderName = '.vcsgib_test_reset_deletion';
303
+ try {
304
+ await executeInitCmd({
305
+ targetDir: testDir,
306
+ vcsFolderName,
307
+ quiet: true,
308
+ });
309
+
310
+ await fs.writeFile(path.join(testDir, 'important.txt'), 'Important data', 'utf8');
311
+
312
+ await executeAddCmd({
313
+ paths: ['important.txt'],
314
+ targetDir: testDir,
315
+ vcsFolderName,
316
+ quiet: true,
317
+ });
318
+
319
+ await executeCommitCmd({
320
+ message: 'Add important file',
321
+ targetDir: testDir,
322
+ vcsFolderName,
323
+ quiet: true,
324
+ });
325
+
326
+ // Delete from disk
327
+ await fs.unlink(path.join(testDir, 'important.txt'));
328
+
329
+ // Stage deletion
330
+ await executeAddCmd({
331
+ paths: ['important.txt'],
332
+ targetDir: testDir,
333
+ vcsFolderName,
334
+ quiet: true,
335
+ });
336
+
337
+ let statusRes = await executeStatusCmd({
338
+ targetDir: testDir,
339
+ vcsFolderName,
340
+ quiet: true,
341
+ });
342
+ iReckon(lc, statusRes.staged.map(x => x.path)).includes('important.txt');
343
+
344
+ // Reset (unstage) deletion
345
+ const resetRes = await executeResetCmd({
346
+ paths: ['important.txt'],
347
+ targetDir: testDir,
348
+ vcsFolderName,
349
+ quiet: true,
350
+ });
351
+ iReckon(lc, resetRes.unstagedPaths).includes('important.txt');
352
+
353
+ statusRes = await executeStatusCmd({
354
+ targetDir: testDir,
355
+ vcsFolderName,
356
+ quiet: true,
357
+ });
358
+ // Should no longer be staged, but still unstaged deleted
359
+ iReckon(lc, statusRes.staged.length).willEqual(0);
360
+ iReckon(lc, statusRes.deleted.map(x => x.path)).includes('important.txt');
361
+ } finally {
362
+ await fs.rm(testDir, { recursive: true, force: true });
363
+ }
364
+ });
365
+
366
+ });
@@ -120,6 +120,7 @@ export async function main({
120
120
  case VCS_SUBCOMMANDS.COMMIT: {
121
121
  await executeCommitCmd({
122
122
  message: parsed.flags.message,
123
+ keepEmptyFolders: parsed.flags.keepEmptyFolders,
123
124
  });
124
125
  break;
125
126
  }