@lumenflow/cli 2.20.1 → 2.21.1

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 (111) hide show
  1. package/README.md +8 -4
  2. package/dist/hooks/enforcement-checks.js +120 -0
  3. package/dist/hooks/enforcement-checks.js.map +1 -1
  4. package/dist/init-lane-validation.js +141 -0
  5. package/dist/init-lane-validation.js.map +1 -0
  6. package/dist/init-templates.js +36 -8
  7. package/dist/init-templates.js.map +1 -1
  8. package/dist/init.js +27 -58
  9. package/dist/init.js.map +1 -1
  10. package/dist/initiative-create.js +35 -4
  11. package/dist/initiative-create.js.map +1 -1
  12. package/dist/lane-lifecycle-process.js +364 -0
  13. package/dist/lane-lifecycle-process.js.map +1 -0
  14. package/dist/lane-lock.js +41 -0
  15. package/dist/lane-lock.js.map +1 -0
  16. package/dist/lane-setup.js +55 -0
  17. package/dist/lane-setup.js.map +1 -0
  18. package/dist/lane-status.js +38 -0
  19. package/dist/lane-status.js.map +1 -0
  20. package/dist/lane-validate.js +43 -0
  21. package/dist/lane-validate.js.map +1 -0
  22. package/dist/onboarding-smoke-test.js +17 -0
  23. package/dist/onboarding-smoke-test.js.map +1 -1
  24. package/dist/public-manifest.js +28 -0
  25. package/dist/public-manifest.js.map +1 -1
  26. package/dist/wu-claim-cloud.js +16 -0
  27. package/dist/wu-claim-cloud.js.map +1 -1
  28. package/dist/wu-claim.js +12 -2
  29. package/dist/wu-claim.js.map +1 -1
  30. package/dist/wu-create-content.js +8 -2
  31. package/dist/wu-create-content.js.map +1 -1
  32. package/dist/wu-create-validation.js +5 -3
  33. package/dist/wu-create-validation.js.map +1 -1
  34. package/dist/wu-create.js +21 -1
  35. package/dist/wu-create.js.map +1 -1
  36. package/dist/wu-done.js +57 -8
  37. package/dist/wu-done.js.map +1 -1
  38. package/dist/wu-prep.js +22 -0
  39. package/dist/wu-prep.js.map +1 -1
  40. package/package.json +15 -11
  41. package/dist/__tests__/agent-log-issue.test.js +0 -56
  42. package/dist/__tests__/agent-spawn-coordination.test.js +0 -451
  43. package/dist/__tests__/backlog-prune.test.js +0 -478
  44. package/dist/__tests__/cli-entry-point.test.js +0 -160
  45. package/dist/__tests__/cli-subprocess.test.js +0 -89
  46. package/dist/__tests__/commands/integrate.test.js +0 -165
  47. package/dist/__tests__/commands.test.js +0 -271
  48. package/dist/__tests__/deps-operations.test.js +0 -206
  49. package/dist/__tests__/doctor.test.js +0 -510
  50. package/dist/__tests__/file-operations.test.js +0 -906
  51. package/dist/__tests__/flow-report.test.js +0 -24
  52. package/dist/__tests__/gates-config.test.js +0 -303
  53. package/dist/__tests__/gates-integration-tests.test.js +0 -112
  54. package/dist/__tests__/git-operations.test.js +0 -668
  55. package/dist/__tests__/guard-main-branch.test.js +0 -79
  56. package/dist/__tests__/guards-validation.test.js +0 -416
  57. package/dist/__tests__/hooks/enforcement.test.js +0 -279
  58. package/dist/__tests__/init-config-lanes.test.js +0 -131
  59. package/dist/__tests__/init-docs-structure.test.js +0 -152
  60. package/dist/__tests__/init-greenfield.test.js +0 -247
  61. package/dist/__tests__/init-lane-inference.test.js +0 -125
  62. package/dist/__tests__/init-onboarding-docs.test.js +0 -132
  63. package/dist/__tests__/init-quick-ref.test.js +0 -144
  64. package/dist/__tests__/init-scripts.test.js +0 -207
  65. package/dist/__tests__/init-template-portability.test.js +0 -96
  66. package/dist/__tests__/init.test.js +0 -968
  67. package/dist/__tests__/initiative-add-wu.test.js +0 -490
  68. package/dist/__tests__/initiative-e2e.test.js +0 -442
  69. package/dist/__tests__/initiative-plan-replacement.test.js +0 -161
  70. package/dist/__tests__/initiative-plan.test.js +0 -340
  71. package/dist/__tests__/initiative-remove-wu.test.js +0 -458
  72. package/dist/__tests__/lumenflow-upgrade.test.js +0 -260
  73. package/dist/__tests__/mem-cleanup-execution.test.js +0 -19
  74. package/dist/__tests__/memory-integration.test.js +0 -333
  75. package/dist/__tests__/merge-block.test.js +0 -220
  76. package/dist/__tests__/metrics-cli.test.js +0 -619
  77. package/dist/__tests__/metrics-snapshot.test.js +0 -24
  78. package/dist/__tests__/no-beacon-references-docs.test.js +0 -30
  79. package/dist/__tests__/no-beacon-references.test.js +0 -39
  80. package/dist/__tests__/onboarding-smoke-test.test.js +0 -211
  81. package/dist/__tests__/path-centralization-cli.test.js +0 -234
  82. package/dist/__tests__/plan-create.test.js +0 -126
  83. package/dist/__tests__/plan-edit.test.js +0 -157
  84. package/dist/__tests__/plan-link.test.js +0 -239
  85. package/dist/__tests__/plan-promote.test.js +0 -181
  86. package/dist/__tests__/release.test.js +0 -372
  87. package/dist/__tests__/rotate-progress.test.js +0 -127
  88. package/dist/__tests__/safe-git.test.js +0 -190
  89. package/dist/__tests__/session-coordinator.test.js +0 -109
  90. package/dist/__tests__/state-bootstrap.test.js +0 -432
  91. package/dist/__tests__/state-doctor.test.js +0 -328
  92. package/dist/__tests__/sync-templates.test.js +0 -255
  93. package/dist/__tests__/templates-sync.test.js +0 -219
  94. package/dist/__tests__/trace-gen.test.js +0 -115
  95. package/dist/__tests__/wu-create-required-fields.test.js +0 -143
  96. package/dist/__tests__/wu-create-strict.test.js +0 -118
  97. package/dist/__tests__/wu-create.test.js +0 -121
  98. package/dist/__tests__/wu-done-auto-cleanup.test.js +0 -135
  99. package/dist/__tests__/wu-done-docs-only-policy.test.js +0 -20
  100. package/dist/__tests__/wu-done-staging-whitelist.test.js +0 -35
  101. package/dist/__tests__/wu-done.test.js +0 -36
  102. package/dist/__tests__/wu-edit-strict.test.js +0 -109
  103. package/dist/__tests__/wu-edit.test.js +0 -119
  104. package/dist/__tests__/wu-lifecycle-integration.test.js +0 -388
  105. package/dist/__tests__/wu-prep-default-exec.test.js +0 -35
  106. package/dist/__tests__/wu-prep.test.js +0 -140
  107. package/dist/__tests__/wu-proto.test.js +0 -97
  108. package/dist/__tests__/wu-validate-strict.test.js +0 -113
  109. package/dist/__tests__/wu-validate.test.js +0 -36
  110. package/dist/spawn-list.js +0 -143
  111. package/dist/spawn-list.js.map +0 -1
@@ -1,328 +0,0 @@
1
- /**
2
- * State Doctor CLI Tests (WU-1230)
3
- *
4
- * Tests for state:doctor --fix functionality:
5
- * - Micro-worktree isolation for all tracked file changes
6
- * - Removal of stale WU references from backlog.md and status.md
7
- * - Changes pushed via merge, not direct file modification
8
- */
9
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
- import { join } from 'node:path';
11
- import { mkdtempSync, rmSync, writeFileSync, mkdirSync, readFileSync } from 'node:fs';
12
- import { tmpdir } from 'node:os';
13
- /**
14
- * Mocked modules
15
- */
16
- vi.mock('@lumenflow/core/dist/micro-worktree.js', () => ({
17
- withMicroWorktree: vi.fn(),
18
- }));
19
- vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
20
- getGitForCwd: vi.fn(() => ({
21
- fetch: vi.fn(),
22
- merge: vi.fn(),
23
- push: vi.fn(),
24
- })),
25
- createGitForPath: vi.fn(() => ({
26
- add: vi.fn(),
27
- commit: vi.fn(),
28
- push: vi.fn(),
29
- })),
30
- }));
31
- /**
32
- * Import after mocks are set up
33
- */
34
- import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
35
- /**
36
- * Constants for test paths
37
- */
38
- const LUMENFLOW_DIR = '.lumenflow';
39
- const STATE_DIR = 'state';
40
- const STAMPS_DIR = 'stamps';
41
- const MEMORY_DIR = 'memory';
42
- const DOCS_TASKS_DIR = 'docs/04-operations/tasks';
43
- const BACKLOG_PATH = `${DOCS_TASKS_DIR}/backlog.md`;
44
- const STATUS_PATH = `${DOCS_TASKS_DIR}/status.md`;
45
- /**
46
- * Test directory path
47
- */
48
- let testDir;
49
- describe('state-doctor CLI (WU-1230)', () => {
50
- beforeEach(() => {
51
- vi.clearAllMocks();
52
- // Create temp directory for each test
53
- testDir = mkdtempSync(join(tmpdir(), 'state-doctor-test-'));
54
- });
55
- afterEach(() => {
56
- // Cleanup temp directory
57
- try {
58
- rmSync(testDir, { recursive: true, force: true });
59
- }
60
- catch {
61
- // Ignore cleanup errors
62
- }
63
- });
64
- describe('micro-worktree isolation', () => {
65
- it('should use micro-worktree when --fix modifies tracked files', async () => {
66
- // Setup: Create test state with broken events
67
- setupTestState(testDir, {
68
- wus: [],
69
- events: [{ wuId: 'WU-999', type: 'claimed', timestamp: new Date().toISOString() }],
70
- });
71
- // Mock withMicroWorktree to track that it was called
72
- const mockWithMicroWorktree = vi.mocked(withMicroWorktree);
73
- mockWithMicroWorktree.mockImplementation(async (options) => {
74
- // Execute the callback to simulate micro-worktree operations
75
- const result = await options.execute({
76
- worktreePath: testDir,
77
- gitWorktree: {
78
- add: vi.fn(),
79
- addWithDeletions: vi.fn(),
80
- commit: vi.fn(),
81
- push: vi.fn(),
82
- },
83
- });
84
- return { ...result, ref: 'main' };
85
- });
86
- // Import and run the fix function
87
- const { createStateDoctorFixDeps } = await import('../state-doctor-fix.js');
88
- const deps = createStateDoctorFixDeps(testDir);
89
- // When: Attempt to remove a broken event
90
- await deps.removeEvent('WU-999');
91
- // Then: micro-worktree should have been used
92
- expect(mockWithMicroWorktree).toHaveBeenCalled();
93
- expect(mockWithMicroWorktree).toHaveBeenCalledWith(expect.objectContaining({
94
- operation: 'state-doctor',
95
- pushOnly: true,
96
- }));
97
- });
98
- it('should not directly modify files on main when --fix is used', async () => {
99
- // Setup: Create test state with events file
100
- const eventsPath = join(testDir, LUMENFLOW_DIR, STATE_DIR, 'wu-events.jsonl');
101
- setupTestState(testDir, {
102
- wus: [],
103
- events: [{ wuId: 'WU-999', type: 'claimed', timestamp: new Date().toISOString() }],
104
- });
105
- const originalContent = readFileSync(eventsPath, 'utf-8');
106
- // Mock withMicroWorktree to NOT actually modify files (simulating push-only mode)
107
- const mockWithMicroWorktree = vi.mocked(withMicroWorktree);
108
- mockWithMicroWorktree.mockResolvedValue({
109
- commitMessage: 'fix: remove broken events',
110
- files: ['.lumenflow/state/wu-events.jsonl'],
111
- ref: 'main',
112
- });
113
- // Import and run the fix function
114
- const { createStateDoctorFixDeps } = await import('../state-doctor-fix.js');
115
- const deps = createStateDoctorFixDeps(testDir);
116
- // When: Remove broken event
117
- await deps.removeEvent('WU-999');
118
- // Then: Original file on main should be unchanged
119
- // (changes only happen in micro-worktree and pushed)
120
- const currentContent = readFileSync(eventsPath, 'utf-8');
121
- expect(currentContent).toBe(originalContent);
122
- });
123
- });
124
- describe('backlog.md and status.md cleanup', () => {
125
- it('should remove stale WU references from backlog.md when removing broken events', async () => {
126
- // Setup: Create backlog.md with reference to WU that will be removed
127
- setupTestState(testDir, {
128
- wus: [],
129
- events: [{ wuId: 'WU-999', type: 'claimed', timestamp: new Date().toISOString() }],
130
- backlog: `# Backlog
131
-
132
- ## In Progress
133
-
134
- - WU-999: Some old WU that no longer exists
135
-
136
- ## Ready
137
-
138
- - WU-100: Valid WU
139
- `,
140
- });
141
- // Track the files that would be modified in micro-worktree
142
- let capturedFiles = [];
143
- const mockWithMicroWorktree = vi.mocked(withMicroWorktree);
144
- mockWithMicroWorktree.mockImplementation(async (options) => {
145
- const result = await options.execute({
146
- worktreePath: testDir,
147
- gitWorktree: {
148
- add: vi.fn(),
149
- addWithDeletions: vi.fn(),
150
- commit: vi.fn(),
151
- push: vi.fn(),
152
- },
153
- });
154
- capturedFiles = result.files;
155
- return { ...result, ref: 'main' };
156
- });
157
- // Import and run the fix function
158
- const { createStateDoctorFixDeps } = await import('../state-doctor-fix.js');
159
- const deps = createStateDoctorFixDeps(testDir);
160
- // When: Remove broken event for WU-999
161
- await deps.removeEvent('WU-999');
162
- // Then: backlog.md should be in the list of modified files
163
- expect(capturedFiles).toContain(BACKLOG_PATH);
164
- });
165
- it('should remove stale WU references from status.md when removing broken events', async () => {
166
- // Setup: Create status.md with reference to WU that will be removed
167
- setupTestState(testDir, {
168
- wus: [],
169
- events: [{ wuId: 'WU-999', type: 'claimed', timestamp: new Date().toISOString() }],
170
- status: `# Status
171
-
172
- ## In Progress
173
-
174
- | Lane | WU | Title |
175
- |------|-----|-------|
176
- | Framework: CLI | WU-999 | Old WU |
177
- `,
178
- });
179
- // Track the files that would be modified
180
- let capturedFiles = [];
181
- const mockWithMicroWorktree = vi.mocked(withMicroWorktree);
182
- mockWithMicroWorktree.mockImplementation(async (options) => {
183
- const result = await options.execute({
184
- worktreePath: testDir,
185
- gitWorktree: {
186
- add: vi.fn(),
187
- addWithDeletions: vi.fn(),
188
- commit: vi.fn(),
189
- push: vi.fn(),
190
- },
191
- });
192
- capturedFiles = result.files;
193
- return { ...result, ref: 'main' };
194
- });
195
- // Import and run the fix function
196
- const { createStateDoctorFixDeps } = await import('../state-doctor-fix.js');
197
- const deps = createStateDoctorFixDeps(testDir);
198
- // When: Remove broken event for WU-999
199
- await deps.removeEvent('WU-999');
200
- // Then: status.md should be in the list of modified files
201
- expect(capturedFiles).toContain(STATUS_PATH);
202
- });
203
- });
204
- describe('commit and push behavior', () => {
205
- it('should use pushOnly mode to avoid modifying local main', async () => {
206
- setupTestState(testDir, {
207
- wus: [],
208
- events: [{ wuId: 'WU-999', type: 'claimed', timestamp: new Date().toISOString() }],
209
- });
210
- const mockWithMicroWorktree = vi.mocked(withMicroWorktree);
211
- mockWithMicroWorktree.mockImplementation(async (options) => {
212
- // Verify pushOnly is set
213
- expect(options.pushOnly).toBe(true);
214
- const result = await options.execute({
215
- worktreePath: testDir,
216
- gitWorktree: {
217
- add: vi.fn(),
218
- addWithDeletions: vi.fn(),
219
- commit: vi.fn(),
220
- push: vi.fn(),
221
- },
222
- });
223
- return { ...result, ref: 'main' };
224
- });
225
- const { createStateDoctorFixDeps } = await import('../state-doctor-fix.js');
226
- const deps = createStateDoctorFixDeps(testDir);
227
- await deps.removeEvent('WU-999');
228
- // The assertion is inside the mock - if we get here without error, pushOnly was true
229
- expect(mockWithMicroWorktree).toHaveBeenCalled();
230
- });
231
- });
232
- // WU-1362: Retry logic for push failures
233
- describe('WU-1362: retry logic for push failures', () => {
234
- it('should retry on push failure with exponential backoff', async () => {
235
- setupTestState(testDir, {
236
- wus: [],
237
- events: [{ wuId: 'WU-999', type: 'claimed', timestamp: new Date().toISOString() }],
238
- });
239
- let callCount = 0;
240
- const mockWithMicroWorktree = vi.mocked(withMicroWorktree);
241
- mockWithMicroWorktree.mockImplementation(async (options) => {
242
- callCount++;
243
- // Simulate the micro-worktree handling retries internally
244
- const result = await options.execute({
245
- worktreePath: testDir,
246
- gitWorktree: {
247
- add: vi.fn(),
248
- addWithDeletions: vi.fn(),
249
- commit: vi.fn(),
250
- push: vi.fn(),
251
- },
252
- });
253
- return { ...result, ref: 'main' };
254
- });
255
- const { createStateDoctorFixDeps } = await import('../state-doctor-fix.js');
256
- const deps = createStateDoctorFixDeps(testDir);
257
- // removeEvent should succeed (micro-worktree handles retry internally)
258
- await deps.removeEvent('WU-999');
259
- expect(callCount).toBe(1);
260
- });
261
- it('should use maxRetries configuration from config', async () => {
262
- setupTestState(testDir, {
263
- wus: [],
264
- events: [{ wuId: 'WU-999', type: 'claimed', timestamp: new Date().toISOString() }],
265
- });
266
- const mockWithMicroWorktree = vi.mocked(withMicroWorktree);
267
- mockWithMicroWorktree.mockImplementation(async (options) => {
268
- // Verify retries is set (micro-worktree handles retry logic)
269
- const result = await options.execute({
270
- worktreePath: testDir,
271
- gitWorktree: {
272
- add: vi.fn(),
273
- addWithDeletions: vi.fn(),
274
- commit: vi.fn(),
275
- push: vi.fn(),
276
- },
277
- });
278
- return { ...result, ref: 'main' };
279
- });
280
- const { createStateDoctorFixDeps } = await import('../state-doctor-fix.js');
281
- const deps = createStateDoctorFixDeps(testDir);
282
- await deps.removeEvent('WU-999');
283
- expect(mockWithMicroWorktree).toHaveBeenCalled();
284
- });
285
- });
286
- });
287
- function setupTestState(baseDir, state) {
288
- // Create directories
289
- const dirs = [
290
- join(baseDir, LUMENFLOW_DIR, STATE_DIR),
291
- join(baseDir, LUMENFLOW_DIR, STAMPS_DIR),
292
- join(baseDir, LUMENFLOW_DIR, MEMORY_DIR),
293
- join(baseDir, DOCS_TASKS_DIR, 'wu'),
294
- ];
295
- for (const dir of dirs) {
296
- mkdirSync(dir, { recursive: true });
297
- }
298
- // Create events file
299
- if (state.events && state.events.length > 0) {
300
- const eventsPath = join(baseDir, LUMENFLOW_DIR, STATE_DIR, 'wu-events.jsonl');
301
- const content = state.events.map((e) => JSON.stringify(e)).join('\n') + '\n';
302
- writeFileSync(eventsPath, content, 'utf-8');
303
- }
304
- // Create signals file
305
- if (state.signals && state.signals.length > 0) {
306
- const signalsPath = join(baseDir, LUMENFLOW_DIR, MEMORY_DIR, 'signals.jsonl');
307
- const content = state.signals.map((s) => JSON.stringify(s)).join('\n') + '\n';
308
- writeFileSync(signalsPath, content, 'utf-8');
309
- }
310
- // Create WU YAML files
311
- if (state.wus) {
312
- for (const wu of state.wus) {
313
- const wuPath = join(baseDir, DOCS_TASKS_DIR, 'wu', `${wu.id}.yaml`);
314
- const content = `id: ${wu.id}\nstatus: ${wu.status}\ntitle: ${wu.title || wu.id}\n`;
315
- writeFileSync(wuPath, content, 'utf-8');
316
- }
317
- }
318
- // Create backlog.md
319
- if (state.backlog) {
320
- const backlogPath = join(baseDir, BACKLOG_PATH);
321
- writeFileSync(backlogPath, state.backlog, 'utf-8');
322
- }
323
- // Create status.md
324
- if (state.status) {
325
- const statusPath = join(baseDir, STATUS_PATH);
326
- writeFileSync(statusPath, state.status, 'utf-8');
327
- }
328
- }
@@ -1,255 +0,0 @@
1
- /**
2
- * Tests for sync:templates command (WU-1368)
3
- *
4
- * Two bugs being fixed:
5
- * 1. --check-drift flag syncs files instead of only checking - should be read-only
6
- * 2. sync:templates writes directly to main checkout - should use micro-worktree isolation
7
- *
8
- * TDD: These tests are written BEFORE the implementation changes.
9
- */
10
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
- import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
12
- import { join } from 'node:path';
13
- import { tmpdir } from 'node:os';
14
- // Mock modules before importing
15
- const mockWithMicroWorktree = vi.fn();
16
- vi.mock('@lumenflow/core/dist/micro-worktree.js', () => ({
17
- withMicroWorktree: mockWithMicroWorktree,
18
- }));
19
- vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
20
- getGitForCwd: vi.fn(() => ({
21
- branch: vi.fn().mockResolvedValue({ current: 'main' }),
22
- status: vi.fn().mockResolvedValue({ isClean: () => true }),
23
- })),
24
- }));
25
- vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
26
- ensureOnMain: vi.fn().mockResolvedValue(undefined),
27
- }));
28
- describe('sync:templates --check-drift', () => {
29
- let tempDir;
30
- let originalCwd;
31
- beforeEach(() => {
32
- tempDir = join(tmpdir(), `sync-templates-test-${Date.now()}`);
33
- mkdirSync(tempDir, { recursive: true });
34
- originalCwd = process.cwd();
35
- process.chdir(tempDir);
36
- // Set up minimal project structure
37
- const templatesDir = join(tempDir, 'packages', '@lumenflow', 'cli', 'templates', 'core');
38
- mkdirSync(templatesDir, { recursive: true });
39
- // Create LUMENFLOW.md source
40
- writeFileSync(join(tempDir, 'LUMENFLOW.md'), '# LumenFlow\n\nLast updated: 2025-01-01\n');
41
- // Create matching template (no drift)
42
- writeFileSync(join(templatesDir, 'LUMENFLOW.md.template'), '# LumenFlow\n\nLast updated: {{DATE}}\n');
43
- });
44
- afterEach(() => {
45
- process.chdir(originalCwd);
46
- if (existsSync(tempDir)) {
47
- rmSync(tempDir, { recursive: true, force: true });
48
- }
49
- vi.clearAllMocks();
50
- });
51
- describe('checkTemplateDrift', () => {
52
- it('should NOT write any files when checking drift', async () => {
53
- const { checkTemplateDrift } = await import('../sync-templates.js');
54
- // Get initial file mtimes
55
- const sourceFile = join(tempDir, 'LUMENFLOW.md');
56
- const templateFile = join(tempDir, 'packages', '@lumenflow', 'cli', 'templates', 'core', 'LUMENFLOW.md.template');
57
- const sourceMtimeBefore = existsSync(sourceFile) ? readFileSync(sourceFile, 'utf-8') : null;
58
- const templateMtimeBefore = existsSync(templateFile)
59
- ? readFileSync(templateFile, 'utf-8')
60
- : null;
61
- // Run check-drift
62
- await checkTemplateDrift(tempDir);
63
- // Verify files were NOT modified
64
- const sourceMtimeAfter = existsSync(sourceFile) ? readFileSync(sourceFile, 'utf-8') : null;
65
- const templateMtimeAfter = existsSync(templateFile)
66
- ? readFileSync(templateFile, 'utf-8')
67
- : null;
68
- expect(sourceMtimeAfter).toBe(sourceMtimeBefore);
69
- expect(templateMtimeAfter).toBe(templateMtimeBefore);
70
- });
71
- it('should return hasDrift=false when templates match source', async () => {
72
- const { checkTemplateDrift } = await import('../sync-templates.js');
73
- const result = await checkTemplateDrift(tempDir);
74
- expect(result.hasDrift).toBe(false);
75
- expect(result.driftingFiles).toHaveLength(0);
76
- });
77
- it('should return hasDrift=true when templates differ from source', async () => {
78
- const { checkTemplateDrift } = await import('../sync-templates.js');
79
- // Create source with different content
80
- writeFileSync(join(tempDir, 'LUMENFLOW.md'), '# LumenFlow Updated\n\nNew content here\n');
81
- const result = await checkTemplateDrift(tempDir);
82
- expect(result.hasDrift).toBe(true);
83
- expect(result.driftingFiles.length).toBeGreaterThan(0);
84
- });
85
- it('should return hasDrift=true when template file is missing', async () => {
86
- const { checkTemplateDrift } = await import('../sync-templates.js');
87
- // Remove template file
88
- const templateFile = join(tempDir, 'packages', '@lumenflow', 'cli', 'templates', 'core', 'LUMENFLOW.md.template');
89
- rmSync(templateFile);
90
- const result = await checkTemplateDrift(tempDir);
91
- expect(result.hasDrift).toBe(true);
92
- expect(result.driftingFiles).toContain('packages/@lumenflow/cli/templates/core/LUMENFLOW.md.template');
93
- });
94
- it('should NOT call withMicroWorktree during drift check', async () => {
95
- const { checkTemplateDrift } = await import('../sync-templates.js');
96
- await checkTemplateDrift(tempDir);
97
- // withMicroWorktree should NOT be called for read-only drift check
98
- expect(mockWithMicroWorktree).not.toHaveBeenCalled();
99
- });
100
- });
101
- describe('exit codes', () => {
102
- it('should exit 1 when drift is detected', async () => {
103
- const { checkTemplateDrift } = await import('../sync-templates.js');
104
- // Create drifting source
105
- writeFileSync(join(tempDir, 'LUMENFLOW.md'), '# LumenFlow Updated\n\nDifferent content\n');
106
- const result = await checkTemplateDrift(tempDir);
107
- // The result should indicate drift - CLI will use this to set exit code
108
- expect(result.hasDrift).toBe(true);
109
- });
110
- it('should exit 0 when no drift detected', async () => {
111
- const { checkTemplateDrift } = await import('../sync-templates.js');
112
- const result = await checkTemplateDrift(tempDir);
113
- // The result should indicate no drift - CLI will use this to set exit code
114
- expect(result.hasDrift).toBe(false);
115
- });
116
- });
117
- });
118
- describe('sync:templates (sync mode)', () => {
119
- let tempDir;
120
- let originalCwd;
121
- beforeEach(() => {
122
- tempDir = join(tmpdir(), `sync-templates-test-${Date.now()}`);
123
- mkdirSync(tempDir, { recursive: true });
124
- originalCwd = process.cwd();
125
- process.chdir(tempDir);
126
- // Reset mock
127
- mockWithMicroWorktree.mockReset();
128
- mockWithMicroWorktree.mockImplementation(async ({ execute }) => {
129
- // Simulate micro-worktree by creating temp dir and calling execute
130
- const wtPath = join(tmpdir(), `micro-wt-${Date.now()}`);
131
- mkdirSync(wtPath, { recursive: true });
132
- const result = await execute({
133
- worktreePath: wtPath,
134
- gitWorktree: {
135
- add: vi.fn().mockResolvedValue(undefined),
136
- commit: vi.fn().mockResolvedValue(undefined),
137
- },
138
- });
139
- return { ...result, ref: 'main' };
140
- });
141
- });
142
- afterEach(() => {
143
- process.chdir(originalCwd);
144
- if (existsSync(tempDir)) {
145
- rmSync(tempDir, { recursive: true, force: true });
146
- }
147
- vi.clearAllMocks();
148
- });
149
- describe('micro-worktree isolation', () => {
150
- it('should use withMicroWorktree for sync operations', async () => {
151
- const { syncTemplatesWithWorktree } = await import('../sync-templates.js');
152
- // Set up source files
153
- writeFileSync(join(tempDir, 'LUMENFLOW.md'), '# LumenFlow\n\nContent\n');
154
- mkdirSync(join(tempDir, '.lumenflow'), { recursive: true });
155
- writeFileSync(join(tempDir, '.lumenflow', 'constraints.md'), '# Constraints\n');
156
- await syncTemplatesWithWorktree(tempDir);
157
- // Verify withMicroWorktree was called
158
- expect(mockWithMicroWorktree).toHaveBeenCalledTimes(1);
159
- expect(mockWithMicroWorktree).toHaveBeenCalledWith(expect.objectContaining({
160
- operation: 'sync-templates',
161
- id: expect.any(String),
162
- execute: expect.any(Function),
163
- }));
164
- });
165
- it('should write files to micro-worktree path, not main checkout', async () => {
166
- const { syncTemplatesWithWorktree } = await import('../sync-templates.js');
167
- // Set up source files
168
- writeFileSync(join(tempDir, 'LUMENFLOW.md'), '# LumenFlow\n\nContent\n');
169
- let capturedWorktreePath = null;
170
- mockWithMicroWorktree.mockImplementation(async ({ execute }) => {
171
- const wtPath = join(tmpdir(), `micro-wt-verify-${Date.now()}`);
172
- mkdirSync(wtPath, { recursive: true });
173
- capturedWorktreePath = wtPath;
174
- // Create templates structure in worktree
175
- const templatesDir = join(wtPath, 'packages', '@lumenflow', 'cli', 'templates', 'core');
176
- mkdirSync(templatesDir, { recursive: true });
177
- const result = await execute({
178
- worktreePath: wtPath,
179
- gitWorktree: {
180
- add: vi.fn().mockResolvedValue(undefined),
181
- commit: vi.fn().mockResolvedValue(undefined),
182
- },
183
- });
184
- return { ...result, ref: 'main' };
185
- });
186
- await syncTemplatesWithWorktree(tempDir);
187
- // Verify worktree path was used (not main checkout)
188
- expect(capturedWorktreePath).not.toBeNull();
189
- expect(capturedWorktreePath).not.toBe(tempDir);
190
- expect(capturedWorktreePath.startsWith(tmpdir())).toBe(true);
191
- });
192
- it('should return list of synced files for commit', async () => {
193
- const { syncTemplatesWithWorktree } = await import('../sync-templates.js');
194
- // Set up source files
195
- writeFileSync(join(tempDir, 'LUMENFLOW.md'), '# LumenFlow\n\nContent\n');
196
- let capturedResult = null;
197
- mockWithMicroWorktree.mockImplementation(async ({ execute }) => {
198
- const wtPath = join(tmpdir(), `micro-wt-files-${Date.now()}`);
199
- mkdirSync(wtPath, { recursive: true });
200
- // Create templates structure
201
- const templatesDir = join(wtPath, 'packages', '@lumenflow', 'cli', 'templates', 'core');
202
- mkdirSync(templatesDir, { recursive: true });
203
- const result = await execute({
204
- worktreePath: wtPath,
205
- gitWorktree: {
206
- add: vi.fn().mockResolvedValue(undefined),
207
- commit: vi.fn().mockResolvedValue(undefined),
208
- },
209
- });
210
- capturedResult = result;
211
- return { ...result, ref: 'main' };
212
- });
213
- await syncTemplatesWithWorktree(tempDir);
214
- expect(capturedResult).not.toBeNull();
215
- expect(capturedResult.commitMessage).toContain('sync:templates');
216
- expect(Array.isArray(capturedResult.files)).toBe(true);
217
- });
218
- });
219
- describe('atomic commit', () => {
220
- it('should create atomic commit via micro-worktree pattern', async () => {
221
- const { syncTemplatesWithWorktree } = await import('../sync-templates.js');
222
- // Set up source files
223
- writeFileSync(join(tempDir, 'LUMENFLOW.md'), '# LumenFlow\n\nContent\n');
224
- await syncTemplatesWithWorktree(tempDir);
225
- // Verify withMicroWorktree was called (atomic commit pattern)
226
- expect(mockWithMicroWorktree).toHaveBeenCalled();
227
- // Verify the execute function returns proper commit info
228
- const callArgs = mockWithMicroWorktree.mock.calls[0][0];
229
- expect(callArgs.operation).toBe('sync-templates');
230
- });
231
- it('should include timestamp in operation id for uniqueness', async () => {
232
- const { syncTemplatesWithWorktree } = await import('../sync-templates.js');
233
- writeFileSync(join(tempDir, 'LUMENFLOW.md'), '# LumenFlow\n');
234
- await syncTemplatesWithWorktree(tempDir);
235
- const callArgs = mockWithMicroWorktree.mock.calls[0][0];
236
- // ID should be timestamp-based or unique identifier
237
- expect(typeof callArgs.id).toBe('string');
238
- expect(callArgs.id.length).toBeGreaterThan(0);
239
- });
240
- });
241
- });
242
- describe('sync:templates exports', () => {
243
- it('should export checkTemplateDrift function', async () => {
244
- const syncTemplates = await import('../sync-templates.js');
245
- expect(typeof syncTemplates.checkTemplateDrift).toBe('function');
246
- });
247
- it('should export syncTemplatesWithWorktree function', async () => {
248
- const syncTemplates = await import('../sync-templates.js');
249
- expect(typeof syncTemplates.syncTemplatesWithWorktree).toBe('function');
250
- });
251
- it('should export main function for CLI entry', async () => {
252
- const syncTemplates = await import('../sync-templates.js');
253
- expect(typeof syncTemplates.main).toBe('function');
254
- });
255
- });