@lumenflow/cli 1.6.0 → 2.1.0

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 (42) hide show
  1. package/README.md +19 -0
  2. package/dist/__tests__/backlog-prune.test.js +478 -0
  3. package/dist/__tests__/deps-operations.test.js +206 -0
  4. package/dist/__tests__/file-operations.test.js +906 -0
  5. package/dist/__tests__/git-operations.test.js +668 -0
  6. package/dist/__tests__/guards-validation.test.js +416 -0
  7. package/dist/__tests__/init-plan.test.js +340 -0
  8. package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
  9. package/dist/__tests__/metrics-cli.test.js +619 -0
  10. package/dist/__tests__/rotate-progress.test.js +127 -0
  11. package/dist/__tests__/session-coordinator.test.js +109 -0
  12. package/dist/__tests__/state-bootstrap.test.js +432 -0
  13. package/dist/__tests__/trace-gen.test.js +115 -0
  14. package/dist/backlog-prune.js +299 -0
  15. package/dist/deps-add.js +215 -0
  16. package/dist/deps-remove.js +94 -0
  17. package/dist/docs-sync.js +72 -326
  18. package/dist/file-delete.js +236 -0
  19. package/dist/file-edit.js +247 -0
  20. package/dist/file-read.js +197 -0
  21. package/dist/file-write.js +220 -0
  22. package/dist/git-branch.js +187 -0
  23. package/dist/git-diff.js +177 -0
  24. package/dist/git-log.js +230 -0
  25. package/dist/git-status.js +208 -0
  26. package/dist/guard-locked.js +169 -0
  27. package/dist/guard-main-branch.js +202 -0
  28. package/dist/guard-worktree-commit.js +160 -0
  29. package/dist/init-plan.js +337 -0
  30. package/dist/lumenflow-upgrade.js +178 -0
  31. package/dist/metrics-cli.js +433 -0
  32. package/dist/rotate-progress.js +247 -0
  33. package/dist/session-coordinator.js +300 -0
  34. package/dist/state-bootstrap.js +307 -0
  35. package/dist/sync-templates.js +212 -0
  36. package/dist/trace-gen.js +331 -0
  37. package/dist/validate-agent-skills.js +218 -0
  38. package/dist/validate-agent-sync.js +148 -0
  39. package/dist/validate-backlog-sync.js +152 -0
  40. package/dist/validate-skills-spec.js +206 -0
  41. package/dist/validate.js +230 -0
  42. package/package.json +37 -7
@@ -0,0 +1,668 @@
1
+ /**
2
+ * @file git-operations.test.ts
3
+ * @description Tests for git operation CLI commands (WU-1109)
4
+ *
5
+ * Git operations provide WU-aware git wrappers with:
6
+ * - Guard checks for protected branches
7
+ * - Worktree-aware context
8
+ * - Audit logging
9
+ *
10
+ * TDD: RED phase - these tests define expected behavior before implementation
11
+ */
12
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
13
+ import { existsSync } from 'node:fs';
14
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
15
+ import { join } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { tmpdir } from 'node:os';
18
+ import { execSync } from 'node:child_process';
19
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
20
+ // Test imports - these will fail until implementation exists (RED phase)
21
+ import { getGitStatus, parseGitStatusArgs, } from '../git-status.js';
22
+ import { getGitDiff, parseGitDiffArgs } from '../git-diff.js';
23
+ import { getGitLog, parseGitLogArgs } from '../git-log.js';
24
+ import { getGitBranch, parseGitBranchArgs, } from '../git-branch.js';
25
+ import { guardMainBranch, parseGuardMainBranchArgs, } from '../guard-main-branch.js';
26
+ // ============================================================================
27
+ // GIT-STATUS TESTS
28
+ // ============================================================================
29
+ describe('git-status CLI', () => {
30
+ describe('source file existence', () => {
31
+ it('should have the CLI source file', () => {
32
+ const srcPath = join(__dirname, '../git-status.ts');
33
+ expect(existsSync(srcPath)).toBe(true);
34
+ });
35
+ it('should be buildable (dist file exists after build)', () => {
36
+ const distPath = join(__dirname, '../../dist/git-status.js');
37
+ expect(existsSync(distPath)).toBe(true);
38
+ });
39
+ });
40
+ describe('parseGitStatusArgs', () => {
41
+ it('should parse --porcelain flag', () => {
42
+ const args = parseGitStatusArgs(['node', 'git-status', '--porcelain']);
43
+ expect(args.porcelain).toBe(true);
44
+ });
45
+ it('should parse --short flag', () => {
46
+ const args = parseGitStatusArgs(['node', 'git-status', '--short']);
47
+ expect(args.short).toBe(true);
48
+ });
49
+ it('should parse --help flag', () => {
50
+ const args = parseGitStatusArgs(['node', 'git-status', '--help']);
51
+ expect(args.help).toBe(true);
52
+ });
53
+ it('should parse path argument', () => {
54
+ const args = parseGitStatusArgs(['node', 'git-status', 'src/']);
55
+ expect(args.path).toBe('src/');
56
+ });
57
+ it('should default to no flags', () => {
58
+ const args = parseGitStatusArgs(['node', 'git-status']);
59
+ expect(args.porcelain).toBe(false);
60
+ expect(args.short).toBe(false);
61
+ });
62
+ });
63
+ describe('getGitStatus', () => {
64
+ let tempDir;
65
+ beforeEach(async () => {
66
+ tempDir = join(tmpdir(), `git-status-test-${Date.now()}`);
67
+ await mkdir(tempDir, { recursive: true });
68
+ // Initialize a git repo
69
+ execSync('git init', { cwd: tempDir, stdio: 'ignore' });
70
+ execSync('git config user.email "test@example.com"', { cwd: tempDir, stdio: 'ignore' });
71
+ execSync('git config user.name "Test User"', { cwd: tempDir, stdio: 'ignore' });
72
+ });
73
+ afterEach(async () => {
74
+ await rm(tempDir, { recursive: true, force: true });
75
+ });
76
+ it('should return success for clean repo', async () => {
77
+ const result = await getGitStatus({ baseDir: tempDir });
78
+ expect(result.success).toBe(true);
79
+ expect(result.isClean).toBe(true);
80
+ });
81
+ it('should detect untracked files', async () => {
82
+ await writeFile(join(tempDir, 'new-file.txt'), 'content');
83
+ const result = await getGitStatus({ baseDir: tempDir });
84
+ expect(result.success).toBe(true);
85
+ expect(result.isClean).toBe(false);
86
+ expect(result.untracked).toContain('new-file.txt');
87
+ });
88
+ it('should detect modified files', async () => {
89
+ await writeFile(join(tempDir, 'file.txt'), 'initial');
90
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
91
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
92
+ await writeFile(join(tempDir, 'file.txt'), 'modified');
93
+ const result = await getGitStatus({ baseDir: tempDir });
94
+ expect(result.success).toBe(true);
95
+ expect(result.isClean).toBe(false);
96
+ expect(result.modified).toContain('file.txt');
97
+ });
98
+ it('should detect staged files', async () => {
99
+ await writeFile(join(tempDir, 'staged.txt'), 'content');
100
+ execSync('git add staged.txt', { cwd: tempDir, stdio: 'ignore' });
101
+ const result = await getGitStatus({ baseDir: tempDir });
102
+ expect(result.success).toBe(true);
103
+ expect(result.staged).toContain('staged.txt');
104
+ });
105
+ it('should return porcelain output when requested', async () => {
106
+ await writeFile(join(tempDir, 'file.txt'), 'content');
107
+ const result = await getGitStatus({ baseDir: tempDir, porcelain: true });
108
+ expect(result.success).toBe(true);
109
+ expect(result.output).toBeDefined();
110
+ expect(result.output).toContain('?? file.txt');
111
+ });
112
+ });
113
+ });
114
+ // ============================================================================
115
+ // GIT-DIFF TESTS
116
+ // ============================================================================
117
+ describe('git-diff CLI', () => {
118
+ describe('source file existence', () => {
119
+ it('should have the CLI source file', () => {
120
+ const srcPath = join(__dirname, '../git-diff.ts');
121
+ expect(existsSync(srcPath)).toBe(true);
122
+ });
123
+ it('should be buildable (dist file exists after build)', () => {
124
+ const distPath = join(__dirname, '../../dist/git-diff.js');
125
+ expect(existsSync(distPath)).toBe(true);
126
+ });
127
+ });
128
+ describe('parseGitDiffArgs', () => {
129
+ it('should parse --staged flag', () => {
130
+ const args = parseGitDiffArgs(['node', 'git-diff', '--staged']);
131
+ expect(args.staged).toBe(true);
132
+ });
133
+ it('should parse --cached flag as alias for staged', () => {
134
+ const args = parseGitDiffArgs(['node', 'git-diff', '--cached']);
135
+ expect(args.staged).toBe(true);
136
+ });
137
+ it('should parse --name-only flag', () => {
138
+ const args = parseGitDiffArgs(['node', 'git-diff', '--name-only']);
139
+ expect(args.nameOnly).toBe(true);
140
+ });
141
+ it('should parse --stat flag', () => {
142
+ const args = parseGitDiffArgs(['node', 'git-diff', '--stat']);
143
+ expect(args.stat).toBe(true);
144
+ });
145
+ it('should parse commit ref', () => {
146
+ const args = parseGitDiffArgs(['node', 'git-diff', 'HEAD~1']);
147
+ expect(args.ref).toBe('HEAD~1');
148
+ });
149
+ it('should parse file path', () => {
150
+ const args = parseGitDiffArgs(['node', 'git-diff', '--', 'src/file.ts']);
151
+ expect(args.path).toBe('src/file.ts');
152
+ });
153
+ it('should parse --help flag', () => {
154
+ const args = parseGitDiffArgs(['node', 'git-diff', '--help']);
155
+ expect(args.help).toBe(true);
156
+ });
157
+ });
158
+ describe('getGitDiff', () => {
159
+ let tempDir;
160
+ beforeEach(async () => {
161
+ tempDir = join(tmpdir(), `git-diff-test-${Date.now()}`);
162
+ await mkdir(tempDir, { recursive: true });
163
+ execSync('git init', { cwd: tempDir, stdio: 'ignore' });
164
+ execSync('git config user.email "test@example.com"', { cwd: tempDir, stdio: 'ignore' });
165
+ execSync('git config user.name "Test User"', { cwd: tempDir, stdio: 'ignore' });
166
+ });
167
+ afterEach(async () => {
168
+ await rm(tempDir, { recursive: true, force: true });
169
+ });
170
+ it('should return empty diff for clean repo', async () => {
171
+ const result = await getGitDiff({ baseDir: tempDir });
172
+ expect(result.success).toBe(true);
173
+ expect(result.hasDiff).toBe(false);
174
+ });
175
+ it('should detect diff in modified files', async () => {
176
+ await writeFile(join(tempDir, 'file.txt'), 'initial');
177
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
178
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
179
+ await writeFile(join(tempDir, 'file.txt'), 'modified');
180
+ const result = await getGitDiff({ baseDir: tempDir });
181
+ expect(result.success).toBe(true);
182
+ expect(result.hasDiff).toBe(true);
183
+ expect(result.diff).toContain('-initial');
184
+ expect(result.diff).toContain('+modified');
185
+ });
186
+ it('should show staged diff when --staged flag is used', async () => {
187
+ await writeFile(join(tempDir, 'file.txt'), 'initial');
188
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
189
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
190
+ await writeFile(join(tempDir, 'file.txt'), 'modified');
191
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
192
+ const result = await getGitDiff({ baseDir: tempDir, staged: true });
193
+ expect(result.success).toBe(true);
194
+ expect(result.hasDiff).toBe(true);
195
+ });
196
+ it('should show only file names when --name-only flag is used', async () => {
197
+ await writeFile(join(tempDir, 'file.txt'), 'initial');
198
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
199
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
200
+ await writeFile(join(tempDir, 'file.txt'), 'modified');
201
+ const result = await getGitDiff({ baseDir: tempDir, nameOnly: true });
202
+ expect(result.success).toBe(true);
203
+ expect(result.files).toContain('file.txt');
204
+ });
205
+ });
206
+ });
207
+ // ============================================================================
208
+ // GIT-LOG TESTS
209
+ // ============================================================================
210
+ describe('git-log CLI', () => {
211
+ describe('source file existence', () => {
212
+ it('should have the CLI source file', () => {
213
+ const srcPath = join(__dirname, '../git-log.ts');
214
+ expect(existsSync(srcPath)).toBe(true);
215
+ });
216
+ it('should be buildable (dist file exists after build)', () => {
217
+ const distPath = join(__dirname, '../../dist/git-log.js');
218
+ expect(existsSync(distPath)).toBe(true);
219
+ });
220
+ });
221
+ describe('parseGitLogArgs', () => {
222
+ it('should parse --oneline flag', () => {
223
+ const args = parseGitLogArgs(['node', 'git-log', '--oneline']);
224
+ expect(args.oneline).toBe(true);
225
+ });
226
+ it('should parse -n/--max-count option', () => {
227
+ const args = parseGitLogArgs(['node', 'git-log', '-n', '5']);
228
+ expect(args.maxCount).toBe(5);
229
+ });
230
+ it('should parse --max-count option', () => {
231
+ const args = parseGitLogArgs(['node', 'git-log', '--max-count', '10']);
232
+ expect(args.maxCount).toBe(10);
233
+ });
234
+ it('should parse --format option', () => {
235
+ const args = parseGitLogArgs(['node', 'git-log', '--format', '%h %s']);
236
+ expect(args.format).toBe('%h %s');
237
+ });
238
+ it('should parse --since option', () => {
239
+ const args = parseGitLogArgs(['node', 'git-log', '--since', '2024-01-01']);
240
+ expect(args.since).toBe('2024-01-01');
241
+ });
242
+ it('should parse --author option', () => {
243
+ const args = parseGitLogArgs(['node', 'git-log', '--author', 'test@example.com']);
244
+ expect(args.author).toBe('test@example.com');
245
+ });
246
+ it('should parse ref argument', () => {
247
+ const args = parseGitLogArgs(['node', 'git-log', 'main..feature']);
248
+ expect(args.ref).toBe('main..feature');
249
+ });
250
+ it('should parse --help flag', () => {
251
+ const args = parseGitLogArgs(['node', 'git-log', '--help']);
252
+ expect(args.help).toBe(true);
253
+ });
254
+ });
255
+ describe('getGitLog', () => {
256
+ let tempDir;
257
+ beforeEach(async () => {
258
+ tempDir = join(tmpdir(), `git-log-test-${Date.now()}`);
259
+ await mkdir(tempDir, { recursive: true });
260
+ execSync('git init', { cwd: tempDir, stdio: 'ignore' });
261
+ execSync('git config user.email "test@example.com"', { cwd: tempDir, stdio: 'ignore' });
262
+ execSync('git config user.name "Test User"', { cwd: tempDir, stdio: 'ignore' });
263
+ });
264
+ afterEach(async () => {
265
+ await rm(tempDir, { recursive: true, force: true });
266
+ });
267
+ it('should return empty log for repo with no commits', async () => {
268
+ const result = await getGitLog({ baseDir: tempDir });
269
+ expect(result.success).toBe(true);
270
+ expect(result.commits).toHaveLength(0);
271
+ });
272
+ it('should return commits when they exist', async () => {
273
+ await writeFile(join(tempDir, 'file.txt'), 'content');
274
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
275
+ execSync('git commit -m "test commit"', { cwd: tempDir, stdio: 'ignore' });
276
+ const result = await getGitLog({ baseDir: tempDir });
277
+ expect(result.success).toBe(true);
278
+ expect(result.commits.length).toBeGreaterThan(0);
279
+ expect(result.commits[0].message).toContain('test commit');
280
+ });
281
+ it('should respect maxCount option', async () => {
282
+ await writeFile(join(tempDir, 'file.txt'), 'content');
283
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
284
+ execSync('git commit -m "commit 1"', { cwd: tempDir, stdio: 'ignore' });
285
+ await writeFile(join(tempDir, 'file2.txt'), 'content');
286
+ execSync('git add file2.txt', { cwd: tempDir, stdio: 'ignore' });
287
+ execSync('git commit -m "commit 2"', { cwd: tempDir, stdio: 'ignore' });
288
+ const result = await getGitLog({ baseDir: tempDir, maxCount: 1 });
289
+ expect(result.success).toBe(true);
290
+ expect(result.commits).toHaveLength(1);
291
+ });
292
+ it('should return oneline output when requested', async () => {
293
+ await writeFile(join(tempDir, 'file.txt'), 'content');
294
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
295
+ execSync('git commit -m "test commit"', { cwd: tempDir, stdio: 'ignore' });
296
+ const result = await getGitLog({ baseDir: tempDir, oneline: true });
297
+ expect(result.success).toBe(true);
298
+ expect(result.output).toBeDefined();
299
+ });
300
+ });
301
+ });
302
+ // ============================================================================
303
+ // GIT-BRANCH TESTS
304
+ // ============================================================================
305
+ describe('git-branch CLI', () => {
306
+ describe('source file existence', () => {
307
+ it('should have the CLI source file', () => {
308
+ const srcPath = join(__dirname, '../git-branch.ts');
309
+ expect(existsSync(srcPath)).toBe(true);
310
+ });
311
+ it('should be buildable (dist file exists after build)', () => {
312
+ const distPath = join(__dirname, '../../dist/git-branch.js');
313
+ expect(existsSync(distPath)).toBe(true);
314
+ });
315
+ });
316
+ describe('parseGitBranchArgs', () => {
317
+ it('should parse --list flag', () => {
318
+ const args = parseGitBranchArgs(['node', 'git-branch', '--list']);
319
+ expect(args.list).toBe(true);
320
+ });
321
+ it('should parse -a/--all flag', () => {
322
+ const args = parseGitBranchArgs(['node', 'git-branch', '-a']);
323
+ expect(args.all).toBe(true);
324
+ });
325
+ it('should parse -r/--remotes flag', () => {
326
+ const args = parseGitBranchArgs(['node', 'git-branch', '-r']);
327
+ expect(args.remotes).toBe(true);
328
+ });
329
+ it('should parse --show-current flag', () => {
330
+ const args = parseGitBranchArgs(['node', 'git-branch', '--show-current']);
331
+ expect(args.showCurrent).toBe(true);
332
+ });
333
+ it('should parse --contains option', () => {
334
+ const args = parseGitBranchArgs(['node', 'git-branch', '--contains', 'abc123']);
335
+ expect(args.contains).toBe('abc123');
336
+ });
337
+ it('should parse --help flag', () => {
338
+ const args = parseGitBranchArgs(['node', 'git-branch', '--help']);
339
+ expect(args.help).toBe(true);
340
+ });
341
+ });
342
+ describe('getGitBranch', () => {
343
+ let tempDir;
344
+ beforeEach(async () => {
345
+ tempDir = join(tmpdir(), `git-branch-test-${Date.now()}`);
346
+ await mkdir(tempDir, { recursive: true });
347
+ execSync('git init', { cwd: tempDir, stdio: 'ignore' });
348
+ execSync('git config user.email "test@example.com"', { cwd: tempDir, stdio: 'ignore' });
349
+ execSync('git config user.name "Test User"', { cwd: tempDir, stdio: 'ignore' });
350
+ });
351
+ afterEach(async () => {
352
+ await rm(tempDir, { recursive: true, force: true });
353
+ });
354
+ it('should return current branch', async () => {
355
+ await writeFile(join(tempDir, 'file.txt'), 'content');
356
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
357
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
358
+ const result = await getGitBranch({ baseDir: tempDir, showCurrent: true });
359
+ expect(result.success).toBe(true);
360
+ expect(result.current).toBeDefined();
361
+ // Git defaults to main or master depending on config
362
+ expect(['main', 'master']).toContain(result.current);
363
+ });
364
+ it('should list all branches', async () => {
365
+ await writeFile(join(tempDir, 'file.txt'), 'content');
366
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
367
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
368
+ execSync('git checkout -b feature-branch', { cwd: tempDir, stdio: 'ignore' });
369
+ execSync('git checkout main || git checkout master', {
370
+ cwd: tempDir,
371
+ stdio: 'ignore',
372
+ shell: '/bin/bash',
373
+ });
374
+ const result = await getGitBranch({ baseDir: tempDir, list: true });
375
+ expect(result.success).toBe(true);
376
+ expect(result.branches).toBeDefined();
377
+ expect(result.branches?.some((b) => b.name === 'feature-branch')).toBe(true);
378
+ });
379
+ });
380
+ });
381
+ // ============================================================================
382
+ // GUARD-MAIN-BRANCH TESTS
383
+ // ============================================================================
384
+ describe('guard-main-branch CLI', () => {
385
+ describe('source file existence', () => {
386
+ it('should have the CLI source file', () => {
387
+ const srcPath = join(__dirname, '../guard-main-branch.ts');
388
+ expect(existsSync(srcPath)).toBe(true);
389
+ });
390
+ it('should be buildable (dist file exists after build)', () => {
391
+ const distPath = join(__dirname, '../../dist/guard-main-branch.js');
392
+ expect(existsSync(distPath)).toBe(true);
393
+ });
394
+ });
395
+ describe('parseGuardMainBranchArgs', () => {
396
+ it('should parse --allow-agent-branch flag', () => {
397
+ const args = parseGuardMainBranchArgs(['node', 'guard-main-branch', '--allow-agent-branch']);
398
+ expect(args.allowAgentBranch).toBe(true);
399
+ });
400
+ it('should parse --strict flag', () => {
401
+ const args = parseGuardMainBranchArgs(['node', 'guard-main-branch', '--strict']);
402
+ expect(args.strict).toBe(true);
403
+ });
404
+ it('should parse --help flag', () => {
405
+ const args = parseGuardMainBranchArgs(['node', 'guard-main-branch', '--help']);
406
+ expect(args.help).toBe(true);
407
+ });
408
+ });
409
+ describe('guardMainBranch', () => {
410
+ let tempDir;
411
+ beforeEach(async () => {
412
+ tempDir = join(tmpdir(), `guard-main-test-${Date.now()}`);
413
+ await mkdir(tempDir, { recursive: true });
414
+ execSync('git init', { cwd: tempDir, stdio: 'ignore' });
415
+ execSync('git config user.email "test@example.com"', { cwd: tempDir, stdio: 'ignore' });
416
+ execSync('git config user.name "Test User"', { cwd: tempDir, stdio: 'ignore' });
417
+ });
418
+ afterEach(async () => {
419
+ await rm(tempDir, { recursive: true, force: true });
420
+ });
421
+ it('should block on main branch', async () => {
422
+ await writeFile(join(tempDir, 'file.txt'), 'content');
423
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
424
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
425
+ const result = await guardMainBranch({ baseDir: tempDir });
426
+ expect(result.success).toBe(true);
427
+ expect(result.isProtected).toBe(true);
428
+ expect(result.currentBranch).toMatch(/^(main|master)$/);
429
+ });
430
+ it('should allow on feature branch', async () => {
431
+ await writeFile(join(tempDir, 'file.txt'), 'content');
432
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
433
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
434
+ execSync('git checkout -b feature-branch', { cwd: tempDir, stdio: 'ignore' });
435
+ const result = await guardMainBranch({ baseDir: tempDir });
436
+ expect(result.success).toBe(true);
437
+ expect(result.isProtected).toBe(false);
438
+ expect(result.currentBranch).toBe('feature-branch');
439
+ });
440
+ it('should block on lane branch (requires worktree)', async () => {
441
+ await writeFile(join(tempDir, 'file.txt'), 'content');
442
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
443
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
444
+ execSync('git checkout -b lane/operations/wu-1234', { cwd: tempDir, stdio: 'ignore' });
445
+ const result = await guardMainBranch({ baseDir: tempDir });
446
+ expect(result.success).toBe(true);
447
+ expect(result.isProtected).toBe(true);
448
+ expect(result.reason).toContain('lane');
449
+ });
450
+ it('should allow agent branch when --allow-agent-branch is set', async () => {
451
+ await writeFile(join(tempDir, 'file.txt'), 'content');
452
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
453
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
454
+ execSync('git checkout -b claude/session-123', { cwd: tempDir, stdio: 'ignore' });
455
+ const result = await guardMainBranch({ baseDir: tempDir, allowAgentBranch: true });
456
+ expect(result.success).toBe(true);
457
+ // Agent branch should be allowed when flag is set
458
+ expect(result.isProtected).toBe(false);
459
+ });
460
+ it('should block agent branch in strict mode', async () => {
461
+ await writeFile(join(tempDir, 'file.txt'), 'content');
462
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
463
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
464
+ execSync('git checkout -b claude/session-123', { cwd: tempDir, stdio: 'ignore' });
465
+ const result = await guardMainBranch({ baseDir: tempDir, strict: true });
466
+ expect(result.success).toBe(true);
467
+ expect(result.isProtected).toBe(true);
468
+ });
469
+ it('should handle --base-dir argument', () => {
470
+ const args = parseGuardMainBranchArgs([
471
+ 'node',
472
+ 'guard-main-branch',
473
+ '--base-dir',
474
+ '/tmp/test',
475
+ ]);
476
+ expect(args.baseDir).toBe('/tmp/test');
477
+ });
478
+ });
479
+ });
480
+ // ============================================================================
481
+ // ADDITIONAL COVERAGE TESTS
482
+ // ============================================================================
483
+ describe('git-status additional coverage', () => {
484
+ let tempDir;
485
+ beforeEach(async () => {
486
+ tempDir = join(tmpdir(), `git-status-extra-${Date.now()}`);
487
+ await mkdir(tempDir, { recursive: true });
488
+ execSync('git init', { cwd: tempDir, stdio: 'ignore' });
489
+ execSync('git config user.email "test@example.com"', { cwd: tempDir, stdio: 'ignore' });
490
+ execSync('git config user.name "Test User"', { cwd: tempDir, stdio: 'ignore' });
491
+ });
492
+ afterEach(async () => {
493
+ await rm(tempDir, { recursive: true, force: true });
494
+ });
495
+ it('should handle deleted files (working tree)', async () => {
496
+ await writeFile(join(tempDir, 'deleted.txt'), 'content');
497
+ execSync('git add deleted.txt', { cwd: tempDir, stdio: 'ignore' });
498
+ execSync('git commit -m "add file"', { cwd: tempDir, stdio: 'ignore' });
499
+ execSync('rm deleted.txt', { cwd: tempDir, stdio: 'ignore' });
500
+ const result = await getGitStatus({ baseDir: tempDir });
501
+ expect(result.success).toBe(true);
502
+ expect(result.deleted).toContain('deleted.txt');
503
+ });
504
+ it('should handle staged deletions', async () => {
505
+ await writeFile(join(tempDir, 'staged-delete.txt'), 'content');
506
+ execSync('git add staged-delete.txt', { cwd: tempDir, stdio: 'ignore' });
507
+ execSync('git commit -m "add file"', { cwd: tempDir, stdio: 'ignore' });
508
+ execSync('git rm staged-delete.txt', { cwd: tempDir, stdio: 'ignore' });
509
+ const result = await getGitStatus({ baseDir: tempDir });
510
+ expect(result.success).toBe(true);
511
+ expect(result.staged).toContain('staged-delete.txt');
512
+ expect(result.deleted).toContain('staged-delete.txt');
513
+ });
514
+ it('should handle renamed files', async () => {
515
+ await writeFile(join(tempDir, 'original.txt'), 'content');
516
+ execSync('git add original.txt', { cwd: tempDir, stdio: 'ignore' });
517
+ execSync('git commit -m "add file"', { cwd: tempDir, stdio: 'ignore' });
518
+ execSync('git mv original.txt renamed.txt', { cwd: tempDir, stdio: 'ignore' });
519
+ const result = await getGitStatus({ baseDir: tempDir });
520
+ expect(result.success).toBe(true);
521
+ expect(result.staged).toContain('renamed.txt');
522
+ });
523
+ it('should handle path argument', async () => {
524
+ await mkdir(join(tempDir, 'subdir'), { recursive: true });
525
+ await writeFile(join(tempDir, 'subdir', 'file.txt'), 'content');
526
+ await writeFile(join(tempDir, 'root.txt'), 'content');
527
+ const result = await getGitStatus({ baseDir: tempDir, path: 'subdir/' });
528
+ expect(result.success).toBe(true);
529
+ // When filtering by path, only files in that path are shown
530
+ expect(result.untracked?.length).toBe(1);
531
+ expect(result.untracked?.[0]).toContain('subdir');
532
+ });
533
+ it('should parse --base-dir argument', () => {
534
+ const args = parseGitStatusArgs(['node', 'git-status', '--base-dir', '/tmp/test']);
535
+ expect(args.baseDir).toBe('/tmp/test');
536
+ });
537
+ it('should handle short format output', async () => {
538
+ await writeFile(join(tempDir, 'file.txt'), 'content');
539
+ const result = await getGitStatus({ baseDir: tempDir, short: true });
540
+ expect(result.success).toBe(true);
541
+ expect(result.output).toBeDefined();
542
+ });
543
+ });
544
+ describe('git-diff additional coverage', () => {
545
+ let tempDir;
546
+ beforeEach(async () => {
547
+ tempDir = join(tmpdir(), `git-diff-extra-${Date.now()}`);
548
+ await mkdir(tempDir, { recursive: true });
549
+ execSync('git init', { cwd: tempDir, stdio: 'ignore' });
550
+ execSync('git config user.email "test@example.com"', { cwd: tempDir, stdio: 'ignore' });
551
+ execSync('git config user.name "Test User"', { cwd: tempDir, stdio: 'ignore' });
552
+ });
553
+ afterEach(async () => {
554
+ await rm(tempDir, { recursive: true, force: true });
555
+ });
556
+ it('should handle stat output', async () => {
557
+ await writeFile(join(tempDir, 'file.txt'), 'initial');
558
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
559
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
560
+ await writeFile(join(tempDir, 'file.txt'), 'modified');
561
+ const result = await getGitDiff({ baseDir: tempDir, stat: true });
562
+ expect(result.success).toBe(true);
563
+ expect(result.stat).toBeDefined();
564
+ });
565
+ it('should handle ref argument', async () => {
566
+ await writeFile(join(tempDir, 'file.txt'), 'initial');
567
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
568
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
569
+ await writeFile(join(tempDir, 'file.txt'), 'modified');
570
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
571
+ execSync('git commit -m "second"', { cwd: tempDir, stdio: 'ignore' });
572
+ const result = await getGitDiff({ baseDir: tempDir, ref: 'HEAD~1' });
573
+ expect(result.success).toBe(true);
574
+ expect(result.hasDiff).toBe(true);
575
+ });
576
+ it('should parse --base-dir argument', () => {
577
+ const args = parseGitDiffArgs(['node', 'git-diff', '--base-dir', '/tmp/test']);
578
+ expect(args.baseDir).toBe('/tmp/test');
579
+ });
580
+ it('should handle path filter after double dash', async () => {
581
+ await writeFile(join(tempDir, 'file1.txt'), 'initial');
582
+ await writeFile(join(tempDir, 'file2.txt'), 'initial');
583
+ execSync('git add .', { cwd: tempDir, stdio: 'ignore' });
584
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
585
+ await writeFile(join(tempDir, 'file1.txt'), 'modified');
586
+ await writeFile(join(tempDir, 'file2.txt'), 'modified');
587
+ const result = await getGitDiff({ baseDir: tempDir, path: 'file1.txt' });
588
+ expect(result.success).toBe(true);
589
+ expect(result.diff).toContain('file1.txt');
590
+ });
591
+ });
592
+ describe('git-log additional coverage', () => {
593
+ let tempDir;
594
+ beforeEach(async () => {
595
+ tempDir = join(tmpdir(), `git-log-extra-${Date.now()}`);
596
+ await mkdir(tempDir, { recursive: true });
597
+ execSync('git init', { cwd: tempDir, stdio: 'ignore' });
598
+ execSync('git config user.email "test@example.com"', { cwd: tempDir, stdio: 'ignore' });
599
+ execSync('git config user.name "Test User"', { cwd: tempDir, stdio: 'ignore' });
600
+ });
601
+ afterEach(async () => {
602
+ await rm(tempDir, { recursive: true, force: true });
603
+ });
604
+ it('should handle custom format', async () => {
605
+ await writeFile(join(tempDir, 'file.txt'), 'content');
606
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
607
+ execSync('git commit -m "test commit"', { cwd: tempDir, stdio: 'ignore' });
608
+ const result = await getGitLog({ baseDir: tempDir, format: '%h %s' });
609
+ expect(result.success).toBe(true);
610
+ expect(result.output).toBeDefined();
611
+ expect(result.output).toContain('test commit');
612
+ });
613
+ it('should handle author filter', async () => {
614
+ await writeFile(join(tempDir, 'file.txt'), 'content');
615
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
616
+ execSync('git commit -m "test commit"', { cwd: tempDir, stdio: 'ignore' });
617
+ const result = await getGitLog({ baseDir: tempDir, author: 'test@example.com' });
618
+ expect(result.success).toBe(true);
619
+ expect(result.commits.length).toBeGreaterThan(0);
620
+ });
621
+ it('should parse -nN format for max count', () => {
622
+ const args = parseGitLogArgs(['node', 'git-log', '-n5']);
623
+ expect(args.maxCount).toBe(5);
624
+ });
625
+ it('should handle --base-dir argument', () => {
626
+ const args = parseGitLogArgs(['node', 'git-log', '--base-dir', '/tmp/test']);
627
+ expect(args.baseDir).toBe('/tmp/test');
628
+ });
629
+ });
630
+ describe('git-branch additional coverage', () => {
631
+ let tempDir;
632
+ beforeEach(async () => {
633
+ tempDir = join(tmpdir(), `git-branch-extra-${Date.now()}`);
634
+ await mkdir(tempDir, { recursive: true });
635
+ execSync('git init', { cwd: tempDir, stdio: 'ignore' });
636
+ execSync('git config user.email "test@example.com"', { cwd: tempDir, stdio: 'ignore' });
637
+ execSync('git config user.name "Test User"', { cwd: tempDir, stdio: 'ignore' });
638
+ });
639
+ afterEach(async () => {
640
+ await rm(tempDir, { recursive: true, force: true });
641
+ });
642
+ it('should handle --all flag', async () => {
643
+ await writeFile(join(tempDir, 'file.txt'), 'content');
644
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
645
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
646
+ const result = await getGitBranch({ baseDir: tempDir, all: true });
647
+ expect(result.success).toBe(true);
648
+ expect(result.branches).toBeDefined();
649
+ });
650
+ it('should handle --base-dir argument', () => {
651
+ const args = parseGitBranchArgs(['node', 'git-branch', '--base-dir', '/tmp/test']);
652
+ expect(args.baseDir).toBe('/tmp/test');
653
+ });
654
+ it('should handle -l alias for --list', () => {
655
+ const args = parseGitBranchArgs(['node', 'git-branch', '-l']);
656
+ expect(args.list).toBe(true);
657
+ });
658
+ it('should mark current branch correctly', async () => {
659
+ await writeFile(join(tempDir, 'file.txt'), 'content');
660
+ execSync('git add file.txt', { cwd: tempDir, stdio: 'ignore' });
661
+ execSync('git commit -m "initial"', { cwd: tempDir, stdio: 'ignore' });
662
+ execSync('git checkout -b feature', { cwd: tempDir, stdio: 'ignore' });
663
+ const result = await getGitBranch({ baseDir: tempDir });
664
+ expect(result.success).toBe(true);
665
+ const currentBranch = result.branches?.find((b) => b.isCurrent);
666
+ expect(currentBranch?.name).toBe('feature');
667
+ });
668
+ });