@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.
- package/README.md +8 -4
- package/dist/hooks/enforcement-checks.js +120 -0
- package/dist/hooks/enforcement-checks.js.map +1 -1
- package/dist/init-lane-validation.js +141 -0
- package/dist/init-lane-validation.js.map +1 -0
- package/dist/init-templates.js +36 -8
- package/dist/init-templates.js.map +1 -1
- package/dist/init.js +27 -58
- package/dist/init.js.map +1 -1
- package/dist/initiative-create.js +35 -4
- package/dist/initiative-create.js.map +1 -1
- package/dist/lane-lifecycle-process.js +364 -0
- package/dist/lane-lifecycle-process.js.map +1 -0
- package/dist/lane-lock.js +41 -0
- package/dist/lane-lock.js.map +1 -0
- package/dist/lane-setup.js +55 -0
- package/dist/lane-setup.js.map +1 -0
- package/dist/lane-status.js +38 -0
- package/dist/lane-status.js.map +1 -0
- package/dist/lane-validate.js +43 -0
- package/dist/lane-validate.js.map +1 -0
- package/dist/onboarding-smoke-test.js +17 -0
- package/dist/onboarding-smoke-test.js.map +1 -1
- package/dist/public-manifest.js +28 -0
- package/dist/public-manifest.js.map +1 -1
- package/dist/wu-claim-cloud.js +16 -0
- package/dist/wu-claim-cloud.js.map +1 -1
- package/dist/wu-claim.js +12 -2
- package/dist/wu-claim.js.map +1 -1
- package/dist/wu-create-content.js +8 -2
- package/dist/wu-create-content.js.map +1 -1
- package/dist/wu-create-validation.js +5 -3
- package/dist/wu-create-validation.js.map +1 -1
- package/dist/wu-create.js +21 -1
- package/dist/wu-create.js.map +1 -1
- package/dist/wu-done.js +57 -8
- package/dist/wu-done.js.map +1 -1
- package/dist/wu-prep.js +22 -0
- package/dist/wu-prep.js.map +1 -1
- package/package.json +15 -11
- package/dist/__tests__/agent-log-issue.test.js +0 -56
- package/dist/__tests__/agent-spawn-coordination.test.js +0 -451
- package/dist/__tests__/backlog-prune.test.js +0 -478
- package/dist/__tests__/cli-entry-point.test.js +0 -160
- package/dist/__tests__/cli-subprocess.test.js +0 -89
- package/dist/__tests__/commands/integrate.test.js +0 -165
- package/dist/__tests__/commands.test.js +0 -271
- package/dist/__tests__/deps-operations.test.js +0 -206
- package/dist/__tests__/doctor.test.js +0 -510
- package/dist/__tests__/file-operations.test.js +0 -906
- package/dist/__tests__/flow-report.test.js +0 -24
- package/dist/__tests__/gates-config.test.js +0 -303
- package/dist/__tests__/gates-integration-tests.test.js +0 -112
- package/dist/__tests__/git-operations.test.js +0 -668
- package/dist/__tests__/guard-main-branch.test.js +0 -79
- package/dist/__tests__/guards-validation.test.js +0 -416
- package/dist/__tests__/hooks/enforcement.test.js +0 -279
- package/dist/__tests__/init-config-lanes.test.js +0 -131
- package/dist/__tests__/init-docs-structure.test.js +0 -152
- package/dist/__tests__/init-greenfield.test.js +0 -247
- package/dist/__tests__/init-lane-inference.test.js +0 -125
- package/dist/__tests__/init-onboarding-docs.test.js +0 -132
- package/dist/__tests__/init-quick-ref.test.js +0 -144
- package/dist/__tests__/init-scripts.test.js +0 -207
- package/dist/__tests__/init-template-portability.test.js +0 -96
- package/dist/__tests__/init.test.js +0 -968
- package/dist/__tests__/initiative-add-wu.test.js +0 -490
- package/dist/__tests__/initiative-e2e.test.js +0 -442
- package/dist/__tests__/initiative-plan-replacement.test.js +0 -161
- package/dist/__tests__/initiative-plan.test.js +0 -340
- package/dist/__tests__/initiative-remove-wu.test.js +0 -458
- package/dist/__tests__/lumenflow-upgrade.test.js +0 -260
- package/dist/__tests__/mem-cleanup-execution.test.js +0 -19
- package/dist/__tests__/memory-integration.test.js +0 -333
- package/dist/__tests__/merge-block.test.js +0 -220
- package/dist/__tests__/metrics-cli.test.js +0 -619
- package/dist/__tests__/metrics-snapshot.test.js +0 -24
- package/dist/__tests__/no-beacon-references-docs.test.js +0 -30
- package/dist/__tests__/no-beacon-references.test.js +0 -39
- package/dist/__tests__/onboarding-smoke-test.test.js +0 -211
- package/dist/__tests__/path-centralization-cli.test.js +0 -234
- package/dist/__tests__/plan-create.test.js +0 -126
- package/dist/__tests__/plan-edit.test.js +0 -157
- package/dist/__tests__/plan-link.test.js +0 -239
- package/dist/__tests__/plan-promote.test.js +0 -181
- package/dist/__tests__/release.test.js +0 -372
- package/dist/__tests__/rotate-progress.test.js +0 -127
- package/dist/__tests__/safe-git.test.js +0 -190
- package/dist/__tests__/session-coordinator.test.js +0 -109
- package/dist/__tests__/state-bootstrap.test.js +0 -432
- package/dist/__tests__/state-doctor.test.js +0 -328
- package/dist/__tests__/sync-templates.test.js +0 -255
- package/dist/__tests__/templates-sync.test.js +0 -219
- package/dist/__tests__/trace-gen.test.js +0 -115
- package/dist/__tests__/wu-create-required-fields.test.js +0 -143
- package/dist/__tests__/wu-create-strict.test.js +0 -118
- package/dist/__tests__/wu-create.test.js +0 -121
- package/dist/__tests__/wu-done-auto-cleanup.test.js +0 -135
- package/dist/__tests__/wu-done-docs-only-policy.test.js +0 -20
- package/dist/__tests__/wu-done-staging-whitelist.test.js +0 -35
- package/dist/__tests__/wu-done.test.js +0 -36
- package/dist/__tests__/wu-edit-strict.test.js +0 -109
- package/dist/__tests__/wu-edit.test.js +0 -119
- package/dist/__tests__/wu-lifecycle-integration.test.js +0 -388
- package/dist/__tests__/wu-prep-default-exec.test.js +0 -35
- package/dist/__tests__/wu-prep.test.js +0 -140
- package/dist/__tests__/wu-proto.test.js +0 -97
- package/dist/__tests__/wu-validate-strict.test.js +0 -113
- package/dist/__tests__/wu-validate.test.js +0 -36
- package/dist/spawn-list.js +0 -143
- 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
|
-
});
|