@lumenflow/cli 2.4.0 → 2.5.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 +11 -8
- package/dist/__tests__/init-config-lanes.test.js +131 -0
- package/dist/__tests__/init-docs-structure.test.js +119 -0
- package/dist/__tests__/init-lane-inference.test.js +125 -0
- package/dist/__tests__/init-onboarding-docs.test.js +132 -0
- package/dist/__tests__/init-quick-ref.test.js +145 -0
- package/dist/__tests__/init-scripts.test.js +207 -0
- package/dist/__tests__/init-template-portability.test.js +97 -0
- package/dist/__tests__/init.test.js +7 -2
- package/dist/__tests__/initiative-add-wu.test.js +420 -0
- package/dist/__tests__/initiative-plan-replacement.test.js +162 -0
- package/dist/__tests__/initiative-remove-wu.test.js +458 -0
- package/dist/__tests__/onboarding-smoke-test.test.js +211 -0
- package/dist/__tests__/path-centralization-cli.test.js +234 -0
- package/dist/__tests__/plan-create.test.js +126 -0
- package/dist/__tests__/plan-edit.test.js +157 -0
- package/dist/__tests__/plan-link.test.js +239 -0
- package/dist/__tests__/plan-promote.test.js +181 -0
- package/dist/__tests__/templates-sync.test.js +219 -0
- package/dist/__tests__/wu-create-strict.test.js +118 -0
- package/dist/__tests__/wu-edit-strict.test.js +109 -0
- package/dist/__tests__/wu-validate-strict.test.js +113 -0
- package/dist/flow-bottlenecks.js +4 -2
- package/dist/gates.js +22 -0
- package/dist/init.js +670 -87
- package/dist/initiative-add-wu.js +112 -16
- package/dist/initiative-remove-wu.js +248 -0
- package/dist/onboarding-smoke-test.js +400 -0
- package/dist/orchestrate-init-status.js +37 -9
- package/dist/orchestrate-initiative.js +10 -4
- package/dist/plan-create.js +199 -0
- package/dist/plan-edit.js +235 -0
- package/dist/plan-link.js +233 -0
- package/dist/plan-promote.js +231 -0
- package/dist/sync-templates.js +137 -5
- package/dist/wu-block.js +16 -5
- package/dist/wu-claim.js +15 -9
- package/dist/wu-create.js +50 -2
- package/dist/wu-deps.js +3 -1
- package/dist/wu-done.js +14 -5
- package/dist/wu-edit.js +35 -0
- package/dist/wu-prep.js +131 -8
- package/dist/wu-spawn.js +14 -1
- package/dist/wu-unblock.js +34 -2
- package/dist/wu-validate.js +25 -17
- package/package.json +11 -7
- package/templates/core/.lumenflow/constraints.md.template +61 -3
- package/templates/core/AGENTS.md.template +2 -2
- package/templates/core/LUMENFLOW.md.template +85 -23
- package/templates/core/ai/onboarding/agent-invocation-guide.md.template +157 -0
- package/templates/core/ai/onboarding/agent-safety-card.md.template +227 -0
- package/templates/core/ai/onboarding/docs-generation.md.template +277 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +49 -7
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +343 -110
- package/templates/core/ai/onboarding/release-process.md.template +8 -2
- package/templates/core/ai/onboarding/starting-prompt.md.template +407 -0
- package/templates/core/ai/onboarding/test-ratchet.md.template +131 -0
- package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +91 -38
- package/templates/core/ai/onboarding/vendor-support.md.template +219 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +13 -1
- package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +14 -16
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +48 -4
- package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +5 -1
- package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +19 -8
- package/dist/__tests__/init-plan.test.js +0 -340
- package/dist/agent-issues-query.d.ts +0 -16
- package/dist/agent-log-issue.d.ts +0 -10
- package/dist/agent-session-end.d.ts +0 -10
- package/dist/agent-session.d.ts +0 -10
- package/dist/backlog-prune.d.ts +0 -84
- package/dist/cli-entry-point.d.ts +0 -8
- package/dist/deps-add.d.ts +0 -91
- package/dist/deps-remove.d.ts +0 -17
- package/dist/docs-sync.d.ts +0 -50
- package/dist/file-delete.d.ts +0 -84
- package/dist/file-edit.d.ts +0 -82
- package/dist/file-read.d.ts +0 -92
- package/dist/file-write.d.ts +0 -90
- package/dist/flow-bottlenecks.d.ts +0 -16
- package/dist/flow-report.d.ts +0 -16
- package/dist/gates.d.ts +0 -94
- package/dist/git-branch.d.ts +0 -65
- package/dist/git-diff.d.ts +0 -58
- package/dist/git-log.d.ts +0 -69
- package/dist/git-status.d.ts +0 -58
- package/dist/guard-locked.d.ts +0 -62
- package/dist/guard-main-branch.d.ts +0 -50
- package/dist/guard-worktree-commit.d.ts +0 -59
- package/dist/index.d.ts +0 -10
- package/dist/init-plan.d.ts +0 -80
- package/dist/init-plan.js +0 -337
- package/dist/init.d.ts +0 -46
- package/dist/initiative-add-wu.d.ts +0 -22
- package/dist/initiative-bulk-assign-wus.d.ts +0 -16
- package/dist/initiative-create.d.ts +0 -28
- package/dist/initiative-edit.d.ts +0 -34
- package/dist/initiative-list.d.ts +0 -12
- package/dist/initiative-status.d.ts +0 -11
- package/dist/lumenflow-upgrade.d.ts +0 -103
- package/dist/mem-checkpoint.d.ts +0 -16
- package/dist/mem-cleanup.d.ts +0 -29
- package/dist/mem-create.d.ts +0 -17
- package/dist/mem-export.d.ts +0 -10
- package/dist/mem-inbox.d.ts +0 -35
- package/dist/mem-init.d.ts +0 -15
- package/dist/mem-ready.d.ts +0 -16
- package/dist/mem-signal.d.ts +0 -16
- package/dist/mem-start.d.ts +0 -16
- package/dist/mem-summarize.d.ts +0 -22
- package/dist/mem-triage.d.ts +0 -22
- package/dist/metrics-cli.d.ts +0 -90
- package/dist/metrics-snapshot.d.ts +0 -18
- package/dist/orchestrate-init-status.d.ts +0 -11
- package/dist/orchestrate-initiative.d.ts +0 -12
- package/dist/orchestrate-monitor.d.ts +0 -11
- package/dist/release.d.ts +0 -117
- package/dist/rotate-progress.d.ts +0 -48
- package/dist/session-coordinator.d.ts +0 -74
- package/dist/spawn-list.d.ts +0 -16
- package/dist/state-bootstrap.d.ts +0 -92
- package/dist/sync-templates.d.ts +0 -52
- package/dist/trace-gen.d.ts +0 -84
- package/dist/validate-agent-skills.d.ts +0 -50
- package/dist/validate-agent-sync.d.ts +0 -36
- package/dist/validate-backlog-sync.d.ts +0 -37
- package/dist/validate-skills-spec.d.ts +0 -40
- package/dist/validate.d.ts +0 -60
- package/dist/wu-block.d.ts +0 -16
- package/dist/wu-claim.d.ts +0 -74
- package/dist/wu-cleanup.d.ts +0 -35
- package/dist/wu-create.d.ts +0 -69
- package/dist/wu-delete.d.ts +0 -21
- package/dist/wu-deps.d.ts +0 -13
- package/dist/wu-done.d.ts +0 -225
- package/dist/wu-edit.d.ts +0 -63
- package/dist/wu-infer-lane.d.ts +0 -17
- package/dist/wu-preflight.d.ts +0 -47
- package/dist/wu-prune.d.ts +0 -16
- package/dist/wu-recover.d.ts +0 -37
- package/dist/wu-release.d.ts +0 -19
- package/dist/wu-repair.d.ts +0 -60
- package/dist/wu-spawn-completion.d.ts +0 -10
- package/dist/wu-spawn.d.ts +0 -192
- package/dist/wu-status.d.ts +0 -25
- package/dist/wu-unblock.d.ts +0 -16
- package/dist/wu-unlock-lane.d.ts +0 -19
- package/dist/wu-validate.d.ts +0 -16
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for plan:link command (WU-1313)
|
|
3
|
+
*
|
|
4
|
+
* The plan:link command links existing plan files to WUs (via spec_refs)
|
|
5
|
+
* or initiatives (via related_plan). This replaces the initiative:plan command.
|
|
6
|
+
*
|
|
7
|
+
* TDD: These tests are written BEFORE the implementation.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
|
|
14
|
+
/** Test constants - avoid sonarjs/no-duplicate-string */
|
|
15
|
+
const TEST_WU_DIR = 'docs/04-operations/tasks/wu';
|
|
16
|
+
const TEST_INIT_DIR = 'docs/04-operations/tasks/initiatives';
|
|
17
|
+
const TEST_PLANS_DIR = 'docs/04-operations/plans';
|
|
18
|
+
const TEST_WU_ID = 'WU-1313';
|
|
19
|
+
const TEST_INIT_ID = 'INIT-001';
|
|
20
|
+
const TEST_WU_PLAN_URI = `lumenflow://plans/${TEST_WU_ID}-plan.md`;
|
|
21
|
+
const TEST_INIT_PLAN_URI = `lumenflow://plans/${TEST_INIT_ID}-plan.md`;
|
|
22
|
+
const TEST_LANE = 'Framework: CLI';
|
|
23
|
+
const TEST_INIT_SLUG = 'test-initiative';
|
|
24
|
+
const TEST_INIT_TITLE = 'Test Initiative';
|
|
25
|
+
const TEST_WU_TITLE = 'Test WU';
|
|
26
|
+
const TEST_STATUS_OPEN = 'open';
|
|
27
|
+
const TEST_DATE = '2026-01-25';
|
|
28
|
+
// Mock modules before importing
|
|
29
|
+
vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
|
|
30
|
+
getGitForCwd: vi.fn(() => ({
|
|
31
|
+
branch: vi.fn().mockResolvedValue({ current: 'main' }),
|
|
32
|
+
status: vi.fn().mockResolvedValue({ isClean: () => true }),
|
|
33
|
+
})),
|
|
34
|
+
}));
|
|
35
|
+
vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
|
|
36
|
+
ensureOnMain: vi.fn().mockResolvedValue(undefined),
|
|
37
|
+
}));
|
|
38
|
+
vi.mock('@lumenflow/core/dist/micro-worktree.js', () => ({
|
|
39
|
+
withMicroWorktree: vi.fn(async ({ execute }) => {
|
|
40
|
+
const tempDir = join(tmpdir(), `plan-link-test-${Date.now()}`);
|
|
41
|
+
mkdirSync(tempDir, { recursive: true });
|
|
42
|
+
return execute({ worktreePath: tempDir });
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
describe('plan:link command', () => {
|
|
46
|
+
let tempDir;
|
|
47
|
+
let originalCwd;
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
tempDir = join(tmpdir(), `plan-link-test-${Date.now()}`);
|
|
50
|
+
mkdirSync(tempDir, { recursive: true });
|
|
51
|
+
originalCwd = process.cwd();
|
|
52
|
+
});
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
process.chdir(originalCwd);
|
|
55
|
+
if (existsSync(tempDir)) {
|
|
56
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
vi.clearAllMocks();
|
|
59
|
+
});
|
|
60
|
+
describe('linkPlanToWU', () => {
|
|
61
|
+
it('should add spec_refs field to WU YAML', async () => {
|
|
62
|
+
const { linkPlanToWU } = await import('../plan-link.js');
|
|
63
|
+
// Setup mock WU file
|
|
64
|
+
const wuDir = join(tempDir, ...TEST_WU_DIR.split('/'));
|
|
65
|
+
mkdirSync(wuDir, { recursive: true });
|
|
66
|
+
const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
|
|
67
|
+
const wuDoc = {
|
|
68
|
+
id: TEST_WU_ID,
|
|
69
|
+
title: TEST_WU_TITLE,
|
|
70
|
+
lane: TEST_LANE,
|
|
71
|
+
status: 'ready',
|
|
72
|
+
type: 'feature',
|
|
73
|
+
};
|
|
74
|
+
writeFileSync(wuPath, stringifyYAML(wuDoc));
|
|
75
|
+
// Link plan
|
|
76
|
+
const changed = linkPlanToWU(tempDir, TEST_WU_ID, TEST_WU_PLAN_URI);
|
|
77
|
+
expect(changed).toBe(true);
|
|
78
|
+
// Verify the file was updated
|
|
79
|
+
const updated = parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
80
|
+
expect(updated.spec_refs).toContain(TEST_WU_PLAN_URI);
|
|
81
|
+
});
|
|
82
|
+
it('should append to existing spec_refs', async () => {
|
|
83
|
+
const { linkPlanToWU } = await import('../plan-link.js');
|
|
84
|
+
// Setup mock WU file with existing spec_refs
|
|
85
|
+
const wuDir = join(tempDir, ...TEST_WU_DIR.split('/'));
|
|
86
|
+
mkdirSync(wuDir, { recursive: true });
|
|
87
|
+
const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
|
|
88
|
+
const wuDoc = {
|
|
89
|
+
id: TEST_WU_ID,
|
|
90
|
+
title: TEST_WU_TITLE,
|
|
91
|
+
lane: TEST_LANE,
|
|
92
|
+
status: 'ready',
|
|
93
|
+
type: 'feature',
|
|
94
|
+
spec_refs: ['lumenflow://plans/existing-plan.md'],
|
|
95
|
+
};
|
|
96
|
+
writeFileSync(wuPath, stringifyYAML(wuDoc));
|
|
97
|
+
// Link additional plan
|
|
98
|
+
const changed = linkPlanToWU(tempDir, TEST_WU_ID, TEST_WU_PLAN_URI);
|
|
99
|
+
expect(changed).toBe(true);
|
|
100
|
+
// Verify both refs are present
|
|
101
|
+
const updated = parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
102
|
+
expect(updated.spec_refs).toContain('lumenflow://plans/existing-plan.md');
|
|
103
|
+
expect(updated.spec_refs).toContain(TEST_WU_PLAN_URI);
|
|
104
|
+
});
|
|
105
|
+
it('should be idempotent if plan already linked', async () => {
|
|
106
|
+
const { linkPlanToWU } = await import('../plan-link.js');
|
|
107
|
+
// Setup mock WU file with plan already linked
|
|
108
|
+
const wuDir = join(tempDir, ...TEST_WU_DIR.split('/'));
|
|
109
|
+
mkdirSync(wuDir, { recursive: true });
|
|
110
|
+
const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
|
|
111
|
+
const wuDoc = {
|
|
112
|
+
id: TEST_WU_ID,
|
|
113
|
+
title: TEST_WU_TITLE,
|
|
114
|
+
lane: TEST_LANE,
|
|
115
|
+
status: 'ready',
|
|
116
|
+
type: 'feature',
|
|
117
|
+
spec_refs: [TEST_WU_PLAN_URI],
|
|
118
|
+
};
|
|
119
|
+
writeFileSync(wuPath, stringifyYAML(wuDoc));
|
|
120
|
+
// Link same plan again
|
|
121
|
+
const changed = linkPlanToWU(tempDir, TEST_WU_ID, TEST_WU_PLAN_URI);
|
|
122
|
+
expect(changed).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
it('should throw if WU not found', async () => {
|
|
125
|
+
const { linkPlanToWU } = await import('../plan-link.js');
|
|
126
|
+
expect(() => linkPlanToWU(tempDir, 'WU-9999', 'lumenflow://plans/plan.md')).toThrow();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe('linkPlanToInitiative', () => {
|
|
130
|
+
it('should add related_plan field to initiative YAML', async () => {
|
|
131
|
+
const { linkPlanToInitiative } = await import('../plan-link.js');
|
|
132
|
+
// Setup mock initiative file
|
|
133
|
+
const initDir = join(tempDir, ...TEST_INIT_DIR.split('/'));
|
|
134
|
+
mkdirSync(initDir, { recursive: true });
|
|
135
|
+
const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
|
|
136
|
+
const initDoc = {
|
|
137
|
+
id: TEST_INIT_ID,
|
|
138
|
+
slug: TEST_INIT_SLUG,
|
|
139
|
+
title: TEST_INIT_TITLE,
|
|
140
|
+
status: TEST_STATUS_OPEN,
|
|
141
|
+
created: TEST_DATE,
|
|
142
|
+
};
|
|
143
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
144
|
+
// Link plan
|
|
145
|
+
const changed = linkPlanToInitiative(tempDir, TEST_INIT_ID, TEST_INIT_PLAN_URI);
|
|
146
|
+
expect(changed).toBe(true);
|
|
147
|
+
// Verify the file was updated
|
|
148
|
+
const updated = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
149
|
+
expect(updated.related_plan).toBe(TEST_INIT_PLAN_URI);
|
|
150
|
+
});
|
|
151
|
+
it('should be idempotent if plan already linked', async () => {
|
|
152
|
+
const { linkPlanToInitiative } = await import('../plan-link.js');
|
|
153
|
+
// Setup mock initiative file with plan already linked
|
|
154
|
+
const initDir = join(tempDir, ...TEST_INIT_DIR.split('/'));
|
|
155
|
+
mkdirSync(initDir, { recursive: true });
|
|
156
|
+
const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
|
|
157
|
+
const initDoc = {
|
|
158
|
+
id: TEST_INIT_ID,
|
|
159
|
+
slug: TEST_INIT_SLUG,
|
|
160
|
+
title: TEST_INIT_TITLE,
|
|
161
|
+
status: TEST_STATUS_OPEN,
|
|
162
|
+
created: TEST_DATE,
|
|
163
|
+
related_plan: TEST_INIT_PLAN_URI,
|
|
164
|
+
};
|
|
165
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
166
|
+
// Link same plan again
|
|
167
|
+
const changed = linkPlanToInitiative(tempDir, TEST_INIT_ID, TEST_INIT_PLAN_URI);
|
|
168
|
+
expect(changed).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
it('should warn but proceed if replacing existing plan', async () => {
|
|
171
|
+
const { linkPlanToInitiative } = await import('../plan-link.js');
|
|
172
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
173
|
+
// Setup mock initiative with different plan
|
|
174
|
+
const initDir = join(tempDir, ...TEST_INIT_DIR.split('/'));
|
|
175
|
+
mkdirSync(initDir, { recursive: true });
|
|
176
|
+
const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
|
|
177
|
+
const initDoc = {
|
|
178
|
+
id: TEST_INIT_ID,
|
|
179
|
+
slug: TEST_INIT_SLUG,
|
|
180
|
+
title: TEST_INIT_TITLE,
|
|
181
|
+
status: TEST_STATUS_OPEN,
|
|
182
|
+
created: TEST_DATE,
|
|
183
|
+
related_plan: 'lumenflow://plans/old-plan.md',
|
|
184
|
+
};
|
|
185
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
186
|
+
// Link new plan
|
|
187
|
+
const changed = linkPlanToInitiative(tempDir, TEST_INIT_ID, 'lumenflow://plans/new-plan.md');
|
|
188
|
+
expect(changed).toBe(true);
|
|
189
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Replacing existing'));
|
|
190
|
+
consoleSpy.mockRestore();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
describe('validatePlanExists', () => {
|
|
194
|
+
it('should pass for existing plan file', async () => {
|
|
195
|
+
const { validatePlanExists } = await import('../plan-link.js');
|
|
196
|
+
const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
|
|
197
|
+
mkdirSync(plansDir, { recursive: true });
|
|
198
|
+
const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
|
|
199
|
+
writeFileSync(planPath, '# Plan');
|
|
200
|
+
expect(() => validatePlanExists(tempDir, TEST_WU_PLAN_URI)).not.toThrow();
|
|
201
|
+
});
|
|
202
|
+
it('should throw for non-existent plan file', async () => {
|
|
203
|
+
const { validatePlanExists } = await import('../plan-link.js');
|
|
204
|
+
expect(() => validatePlanExists(tempDir, 'lumenflow://plans/nonexistent.md')).toThrow();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('resolveTargetType', () => {
|
|
208
|
+
it('should detect WU IDs', async () => {
|
|
209
|
+
const { resolveTargetType } = await import('../plan-link.js');
|
|
210
|
+
expect(resolveTargetType(TEST_WU_ID)).toBe('wu');
|
|
211
|
+
expect(resolveTargetType('WU-001')).toBe('wu');
|
|
212
|
+
expect(resolveTargetType('WU-99999')).toBe('wu');
|
|
213
|
+
});
|
|
214
|
+
it('should detect initiative IDs', async () => {
|
|
215
|
+
const { resolveTargetType } = await import('../plan-link.js');
|
|
216
|
+
expect(resolveTargetType(TEST_INIT_ID)).toBe('initiative');
|
|
217
|
+
expect(resolveTargetType('INIT-TOOLING')).toBe('initiative');
|
|
218
|
+
});
|
|
219
|
+
it('should throw for invalid IDs', async () => {
|
|
220
|
+
const { resolveTargetType } = await import('../plan-link.js');
|
|
221
|
+
expect(() => resolveTargetType('invalid')).toThrow();
|
|
222
|
+
expect(() => resolveTargetType('')).toThrow();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
describe('plan:link CLI exports', () => {
|
|
227
|
+
it('should export main function for CLI entry', async () => {
|
|
228
|
+
const planLink = await import('../plan-link.js');
|
|
229
|
+
expect(typeof planLink.main).toBe('function');
|
|
230
|
+
});
|
|
231
|
+
it('should export all required functions', async () => {
|
|
232
|
+
const planLink = await import('../plan-link.js');
|
|
233
|
+
expect(typeof planLink.linkPlanToWU).toBe('function');
|
|
234
|
+
expect(typeof planLink.linkPlanToInitiative).toBe('function');
|
|
235
|
+
expect(typeof planLink.validatePlanExists).toBe('function');
|
|
236
|
+
expect(typeof planLink.resolveTargetType).toBe('function');
|
|
237
|
+
expect(typeof planLink.LOG_PREFIX).toBe('string');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for plan:promote command (WU-1313)
|
|
3
|
+
*
|
|
4
|
+
* The plan:promote command promotes a plan from draft to approved status,
|
|
5
|
+
* or creates WUs from plan sections.
|
|
6
|
+
*
|
|
7
|
+
* TDD: These tests are written BEFORE the implementation.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
/** Test constants - avoid sonarjs/no-duplicate-string */
|
|
14
|
+
const TEST_PLANS_DIR = 'docs/04-operations/plans';
|
|
15
|
+
const TEST_WU_ID = 'WU-1313';
|
|
16
|
+
// Mock modules before importing
|
|
17
|
+
vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
|
|
18
|
+
getGitForCwd: vi.fn(() => ({
|
|
19
|
+
branch: vi.fn().mockResolvedValue({ current: 'main' }),
|
|
20
|
+
status: vi.fn().mockResolvedValue({ isClean: () => true }),
|
|
21
|
+
})),
|
|
22
|
+
}));
|
|
23
|
+
vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
|
|
24
|
+
ensureOnMain: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
}));
|
|
26
|
+
vi.mock('@lumenflow/core/dist/micro-worktree.js', () => ({
|
|
27
|
+
withMicroWorktree: vi.fn(async ({ execute }) => {
|
|
28
|
+
const tempDir = join(tmpdir(), `plan-promote-test-${Date.now()}`);
|
|
29
|
+
mkdirSync(tempDir, { recursive: true });
|
|
30
|
+
return execute({ worktreePath: tempDir });
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
describe('plan:promote command', () => {
|
|
34
|
+
let tempDir;
|
|
35
|
+
let originalCwd;
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
tempDir = join(tmpdir(), `plan-promote-test-${Date.now()}`);
|
|
38
|
+
mkdirSync(tempDir, { recursive: true });
|
|
39
|
+
originalCwd = process.cwd();
|
|
40
|
+
});
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
process.chdir(originalCwd);
|
|
43
|
+
if (existsSync(tempDir)) {
|
|
44
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
});
|
|
48
|
+
describe('promotePlan', () => {
|
|
49
|
+
it('should add approved status marker to plan', async () => {
|
|
50
|
+
const { promotePlan } = await import('../plan-promote.js');
|
|
51
|
+
// Setup plan file
|
|
52
|
+
const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
|
|
53
|
+
mkdirSync(plansDir, { recursive: true });
|
|
54
|
+
const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
|
|
55
|
+
writeFileSync(planPath, `# WU-1313 Plan
|
|
56
|
+
|
|
57
|
+
Created: 2026-02-01
|
|
58
|
+
|
|
59
|
+
## Goal
|
|
60
|
+
|
|
61
|
+
Implement plan tooling.
|
|
62
|
+
`);
|
|
63
|
+
// Promote plan
|
|
64
|
+
const changed = promotePlan(planPath);
|
|
65
|
+
expect(changed).toBe(true);
|
|
66
|
+
const content = readFileSync(planPath, 'utf-8');
|
|
67
|
+
expect(content).toContain('Status: approved');
|
|
68
|
+
expect(content).toContain('Approved:');
|
|
69
|
+
});
|
|
70
|
+
it('should return false if plan already approved', async () => {
|
|
71
|
+
const { promotePlan } = await import('../plan-promote.js');
|
|
72
|
+
// Setup plan file with approved status
|
|
73
|
+
const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
|
|
74
|
+
mkdirSync(plansDir, { recursive: true });
|
|
75
|
+
const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
|
|
76
|
+
writeFileSync(planPath, `# WU-1313 Plan
|
|
77
|
+
|
|
78
|
+
Created: 2026-02-01
|
|
79
|
+
Status: approved
|
|
80
|
+
Approved: 2026-02-01
|
|
81
|
+
|
|
82
|
+
## Goal
|
|
83
|
+
|
|
84
|
+
Implement plan tooling.
|
|
85
|
+
`);
|
|
86
|
+
// Try to promote again
|
|
87
|
+
const changed = promotePlan(planPath);
|
|
88
|
+
expect(changed).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
it('should throw if plan file not found', async () => {
|
|
91
|
+
const { promotePlan } = await import('../plan-promote.js');
|
|
92
|
+
const planPath = join(tempDir, 'nonexistent.md');
|
|
93
|
+
expect(() => promotePlan(planPath)).toThrow();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('validatePlanComplete', () => {
|
|
97
|
+
it('should pass for complete plan', async () => {
|
|
98
|
+
const { validatePlanComplete } = await import('../plan-promote.js');
|
|
99
|
+
// Setup complete plan file
|
|
100
|
+
const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
|
|
101
|
+
mkdirSync(plansDir, { recursive: true });
|
|
102
|
+
const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
|
|
103
|
+
writeFileSync(planPath, `# WU-1313 Plan
|
|
104
|
+
|
|
105
|
+
Created: 2026-02-01
|
|
106
|
+
|
|
107
|
+
## Goal
|
|
108
|
+
|
|
109
|
+
Clear goal statement here.
|
|
110
|
+
|
|
111
|
+
## Scope
|
|
112
|
+
|
|
113
|
+
- In scope: A
|
|
114
|
+
- Out of scope: B
|
|
115
|
+
|
|
116
|
+
## Approach
|
|
117
|
+
|
|
118
|
+
Step 1: Do X
|
|
119
|
+
Step 2: Do Y
|
|
120
|
+
`);
|
|
121
|
+
const result = validatePlanComplete(planPath);
|
|
122
|
+
expect(result.valid).toBe(true);
|
|
123
|
+
expect(result.errors).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
it('should fail for plan with empty sections', async () => {
|
|
126
|
+
const { validatePlanComplete } = await import('../plan-promote.js');
|
|
127
|
+
// Setup incomplete plan file
|
|
128
|
+
const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
|
|
129
|
+
mkdirSync(plansDir, { recursive: true });
|
|
130
|
+
const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
|
|
131
|
+
writeFileSync(planPath, `# WU-1313 Plan
|
|
132
|
+
|
|
133
|
+
Created: 2026-02-01
|
|
134
|
+
|
|
135
|
+
## Goal
|
|
136
|
+
|
|
137
|
+
## Scope
|
|
138
|
+
|
|
139
|
+
## Approach
|
|
140
|
+
`);
|
|
141
|
+
const result = validatePlanComplete(planPath);
|
|
142
|
+
expect(result.valid).toBe(false);
|
|
143
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
144
|
+
expect(result.errors.some((e) => e.includes('Goal'))).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('getPlanPath', () => {
|
|
148
|
+
it('should resolve plan path from ID', async () => {
|
|
149
|
+
const { getPlanPath } = await import('../plan-promote.js');
|
|
150
|
+
// Setup plan file
|
|
151
|
+
const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
|
|
152
|
+
mkdirSync(plansDir, { recursive: true });
|
|
153
|
+
const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
|
|
154
|
+
writeFileSync(planPath, '# Plan');
|
|
155
|
+
process.chdir(tempDir);
|
|
156
|
+
const resolved = getPlanPath('WU-1313');
|
|
157
|
+
expect(resolved).toContain(`${TEST_WU_ID}-plan.md`);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('getCommitMessage', () => {
|
|
161
|
+
it('should generate correct commit message', async () => {
|
|
162
|
+
const { getCommitMessage } = await import('../plan-promote.js');
|
|
163
|
+
expect(getCommitMessage('WU-1313')).toBe('docs: promote wu-1313 plan to approved');
|
|
164
|
+
expect(getCommitMessage('INIT-001')).toBe('docs: promote init-001 plan to approved');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
describe('plan:promote CLI exports', () => {
|
|
169
|
+
it('should export main function for CLI entry', async () => {
|
|
170
|
+
const planPromote = await import('../plan-promote.js');
|
|
171
|
+
expect(typeof planPromote.main).toBe('function');
|
|
172
|
+
});
|
|
173
|
+
it('should export all required functions', async () => {
|
|
174
|
+
const planPromote = await import('../plan-promote.js');
|
|
175
|
+
expect(typeof planPromote.promotePlan).toBe('function');
|
|
176
|
+
expect(typeof planPromote.validatePlanComplete).toBe('function');
|
|
177
|
+
expect(typeof planPromote.getPlanPath).toBe('function');
|
|
178
|
+
expect(typeof planPromote.getCommitMessage).toBe('function');
|
|
179
|
+
expect(typeof planPromote.LOG_PREFIX).toBe('string');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file templates-sync.test.ts
|
|
3
|
+
* Tests for templates synchronization and drift detection (WU-1353)
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import { syncTemplates, syncOnboardingDocs, syncCoreDocs, convertToTemplate, checkTemplateDrift, } from '../sync-templates.js';
|
|
10
|
+
// Constants for frequently used path segments (sonarjs/no-duplicate-string)
|
|
11
|
+
const PACKAGES_DIR = 'packages';
|
|
12
|
+
const LUMENFLOW_SCOPE = '@lumenflow';
|
|
13
|
+
const CLI_DIR = 'cli';
|
|
14
|
+
const TEMPLATES_DIR = 'templates';
|
|
15
|
+
const CORE_DIR = 'core';
|
|
16
|
+
const LUMENFLOW_DOT_DIR = '.lumenflow';
|
|
17
|
+
const CONSTRAINTS_FILE = 'constraints.md';
|
|
18
|
+
const CONSTRAINTS_TEMPLATE = 'constraints.md.template';
|
|
19
|
+
describe('templates-sync', () => {
|
|
20
|
+
let tempDir;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'templates-sync-test-'));
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
26
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
describe('convertToTemplate', () => {
|
|
30
|
+
it('should replace dates with {{DATE}} placeholder', () => {
|
|
31
|
+
const content = 'Updated: 2026-02-02\nCreated: 2025-01-15';
|
|
32
|
+
const result = convertToTemplate(content, '/home/test/project');
|
|
33
|
+
expect(result).toBe('Updated: {{DATE}}\nCreated: {{DATE}}');
|
|
34
|
+
});
|
|
35
|
+
it('should preserve content without dates', () => {
|
|
36
|
+
const content = '# Title\n\nSome content without dates.';
|
|
37
|
+
const result = convertToTemplate(content, '/home/test/project');
|
|
38
|
+
expect(result).toBe(content);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('syncCoreDocs', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
// Set up directory structure
|
|
44
|
+
const templatesDir = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR);
|
|
45
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
46
|
+
// Create source constraints.md with v1.1 content
|
|
47
|
+
const lumenflowDir = path.join(tempDir, LUMENFLOW_DOT_DIR);
|
|
48
|
+
fs.mkdirSync(lumenflowDir, { recursive: true });
|
|
49
|
+
fs.writeFileSync(path.join(lumenflowDir, CONSTRAINTS_FILE), `# LumenFlow Constraints Capsule
|
|
50
|
+
|
|
51
|
+
**Version:** 1.1
|
|
52
|
+
**Last updated:** 2026-02-02
|
|
53
|
+
|
|
54
|
+
This document contains the 7 non-negotiable constraints.
|
|
55
|
+
|
|
56
|
+
### 1. Worktree Discipline and Git Safety
|
|
57
|
+
|
|
58
|
+
**MANDATORY PRE-WRITE CHECK**
|
|
59
|
+
|
|
60
|
+
**NEVER "QUICK FIX" ON MAIN**
|
|
61
|
+
`);
|
|
62
|
+
// Create LUMENFLOW.md
|
|
63
|
+
fs.writeFileSync(path.join(tempDir, 'LUMENFLOW.md'), `# LumenFlow Workflow Guide
|
|
64
|
+
|
|
65
|
+
**Last updated:** 2026-02-02
|
|
66
|
+
|
|
67
|
+
## Critical Rule: Use wu:prep Then wu:done
|
|
68
|
+
`);
|
|
69
|
+
});
|
|
70
|
+
it('should sync constraints.md to template', async () => {
|
|
71
|
+
const result = await syncCoreDocs(tempDir, false);
|
|
72
|
+
expect(result.errors).toHaveLength(0);
|
|
73
|
+
expect(result.synced).toContain(`${PACKAGES_DIR}/${LUMENFLOW_SCOPE}/${CLI_DIR}/${TEMPLATES_DIR}/${CORE_DIR}/${LUMENFLOW_DOT_DIR}/${CONSTRAINTS_TEMPLATE}`);
|
|
74
|
+
// Verify template content
|
|
75
|
+
const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
|
|
76
|
+
const templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
77
|
+
// Should have {{DATE}} placeholder
|
|
78
|
+
expect(templateContent).toContain('{{DATE}}');
|
|
79
|
+
expect(templateContent).not.toContain('2026-02-02');
|
|
80
|
+
// Should have v1.1 content markers
|
|
81
|
+
expect(templateContent).toContain('Version:** 1.1');
|
|
82
|
+
expect(templateContent).toContain('7 non-negotiable constraints');
|
|
83
|
+
expect(templateContent).toContain('MANDATORY PRE-WRITE CHECK');
|
|
84
|
+
expect(templateContent).toContain('NEVER "QUICK FIX" ON MAIN');
|
|
85
|
+
});
|
|
86
|
+
it('should use dry-run mode without writing files', async () => {
|
|
87
|
+
// First, ensure no template exists
|
|
88
|
+
const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
|
|
89
|
+
// Remove if it exists from beforeEach
|
|
90
|
+
if (fs.existsSync(templatePath)) {
|
|
91
|
+
fs.unlinkSync(templatePath);
|
|
92
|
+
}
|
|
93
|
+
const result = await syncCoreDocs(tempDir, true);
|
|
94
|
+
expect(result.errors).toHaveLength(0);
|
|
95
|
+
expect(result.synced.length).toBeGreaterThan(0);
|
|
96
|
+
expect(fs.existsSync(templatePath)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('syncOnboardingDocs', () => {
|
|
100
|
+
const ONBOARDING_SUBPATH = [
|
|
101
|
+
'docs',
|
|
102
|
+
'04-operations',
|
|
103
|
+
'_frameworks',
|
|
104
|
+
'lumenflow',
|
|
105
|
+
'agent',
|
|
106
|
+
'onboarding',
|
|
107
|
+
];
|
|
108
|
+
const FIRST_WU_MISTAKES_FILE = 'first-wu-mistakes.md';
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
// Set up onboarding source directory
|
|
111
|
+
const onboardingDir = path.join(tempDir, ...ONBOARDING_SUBPATH);
|
|
112
|
+
fs.mkdirSync(onboardingDir, { recursive: true });
|
|
113
|
+
// Create first-wu-mistakes.md with v1.1 content (11 mistakes)
|
|
114
|
+
fs.writeFileSync(path.join(onboardingDir, FIRST_WU_MISTAKES_FILE), `# First WU Mistakes
|
|
115
|
+
|
|
116
|
+
**Last updated:** 2026-02-02
|
|
117
|
+
|
|
118
|
+
## Mistake 1: Not Using Worktrees
|
|
119
|
+
|
|
120
|
+
pnpm wu:prep --id WU-123
|
|
121
|
+
|
|
122
|
+
## Mistake 11: "Quick Fixing" on Main
|
|
123
|
+
|
|
124
|
+
## Quick Checklist
|
|
125
|
+
|
|
126
|
+
- [ ] Check spec_refs for plans
|
|
127
|
+
`);
|
|
128
|
+
// Set up target directory
|
|
129
|
+
const templatesDir = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, 'ai', 'onboarding');
|
|
130
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
131
|
+
});
|
|
132
|
+
it('should sync first-wu-mistakes.md to template', async () => {
|
|
133
|
+
const result = await syncOnboardingDocs(tempDir, false);
|
|
134
|
+
expect(result.errors).toHaveLength(0);
|
|
135
|
+
expect(result.synced).toContain(`${PACKAGES_DIR}/${LUMENFLOW_SCOPE}/${CLI_DIR}/${TEMPLATES_DIR}/${CORE_DIR}/ai/onboarding/${FIRST_WU_MISTAKES_FILE}.template`);
|
|
136
|
+
// Verify template content
|
|
137
|
+
const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, 'ai', 'onboarding', `${FIRST_WU_MISTAKES_FILE}.template`);
|
|
138
|
+
const templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
139
|
+
// Should have {{DATE}} placeholder
|
|
140
|
+
expect(templateContent).toContain('{{DATE}}');
|
|
141
|
+
expect(templateContent).not.toContain('2026-02-02');
|
|
142
|
+
// Should have v1.1 content markers
|
|
143
|
+
expect(templateContent).toContain('Mistake 11:');
|
|
144
|
+
expect(templateContent).toContain('Quick Fixing" on Main');
|
|
145
|
+
expect(templateContent).toContain('wu:prep');
|
|
146
|
+
expect(templateContent).toContain('spec_refs');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('checkTemplateDrift', () => {
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
// Set up source files
|
|
152
|
+
const lumenflowDir = path.join(tempDir, LUMENFLOW_DOT_DIR);
|
|
153
|
+
fs.mkdirSync(lumenflowDir, { recursive: true });
|
|
154
|
+
fs.writeFileSync(path.join(lumenflowDir, CONSTRAINTS_FILE), `# Constraints
|
|
155
|
+
**Version:** 1.1
|
|
156
|
+
**Last updated:** 2026-02-02
|
|
157
|
+
7 constraints`);
|
|
158
|
+
// Set up template directory
|
|
159
|
+
const templatesDir = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR);
|
|
160
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
161
|
+
});
|
|
162
|
+
it('should detect drift when template is outdated', async () => {
|
|
163
|
+
// Create outdated template (v1.0, 6 constraints)
|
|
164
|
+
const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
|
|
165
|
+
fs.writeFileSync(templatePath, `# Constraints
|
|
166
|
+
**Version:** 1.0
|
|
167
|
+
**Last updated:** {{DATE}}
|
|
168
|
+
6 constraints`);
|
|
169
|
+
const drift = await checkTemplateDrift(tempDir);
|
|
170
|
+
expect(drift.hasDrift).toBe(true);
|
|
171
|
+
expect(drift.driftingFiles.length).toBeGreaterThan(0);
|
|
172
|
+
expect(drift.driftingFiles.some((f) => f.includes(CONSTRAINTS_FILE))).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
it('should report no drift when templates are in sync', async () => {
|
|
175
|
+
// First sync templates
|
|
176
|
+
await syncCoreDocs(tempDir, false);
|
|
177
|
+
// Then check for drift
|
|
178
|
+
const drift = await checkTemplateDrift(tempDir);
|
|
179
|
+
// After sync, constraints should not be drifting
|
|
180
|
+
expect(drift.driftingFiles.filter((f) => f.includes(CONSTRAINTS_FILE))).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
it('should return detailed drift report', async () => {
|
|
183
|
+
// Create outdated template
|
|
184
|
+
const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
|
|
185
|
+
fs.writeFileSync(templatePath, 'outdated content');
|
|
186
|
+
const drift = await checkTemplateDrift(tempDir);
|
|
187
|
+
expect(drift.hasDrift).toBe(true);
|
|
188
|
+
expect(drift.driftingFiles).toBeDefined();
|
|
189
|
+
expect(Array.isArray(drift.driftingFiles)).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe('syncTemplates (full sync)', () => {
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
// Set up minimal directory structure
|
|
195
|
+
const lumenflowDir = path.join(tempDir, LUMENFLOW_DOT_DIR);
|
|
196
|
+
fs.mkdirSync(lumenflowDir, { recursive: true });
|
|
197
|
+
fs.writeFileSync(path.join(lumenflowDir, CONSTRAINTS_FILE), 'content');
|
|
198
|
+
fs.writeFileSync(path.join(tempDir, 'LUMENFLOW.md'), 'content');
|
|
199
|
+
const onboardingDir = path.join(tempDir, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding');
|
|
200
|
+
fs.mkdirSync(onboardingDir, { recursive: true });
|
|
201
|
+
fs.writeFileSync(path.join(onboardingDir, 'first-wu-mistakes.md'), 'content');
|
|
202
|
+
const skillsDir = path.join(tempDir, '.claude', 'skills', 'test-skill');
|
|
203
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
204
|
+
fs.writeFileSync(path.join(skillsDir, 'SKILL.md'), 'skill content');
|
|
205
|
+
});
|
|
206
|
+
it('should sync all template categories', async () => {
|
|
207
|
+
const result = await syncTemplates(tempDir, false);
|
|
208
|
+
expect(result.core.errors).toHaveLength(0);
|
|
209
|
+
expect(result.onboarding.errors).toHaveLength(0);
|
|
210
|
+
expect(result.skills.errors).toHaveLength(0);
|
|
211
|
+
// Should sync at least constraints and LUMENFLOW
|
|
212
|
+
expect(result.core.synced.length).toBeGreaterThanOrEqual(2);
|
|
213
|
+
// Should sync onboarding docs
|
|
214
|
+
expect(result.onboarding.synced.length).toBeGreaterThanOrEqual(1);
|
|
215
|
+
// Should sync skills
|
|
216
|
+
expect(result.skills.synced.length).toBeGreaterThanOrEqual(1);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|