@lumenflow/cli 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -105
- package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
- package/dist/__tests__/commands/integrate.test.js +165 -0
- package/dist/__tests__/commands.test.js +75 -0
- package/dist/__tests__/doctor.test.js +510 -0
- package/dist/__tests__/gates-config.test.js +0 -1
- package/dist/__tests__/hooks/enforcement.test.js +279 -0
- package/dist/__tests__/init-greenfield.test.js +247 -0
- package/dist/__tests__/init-quick-ref.test.js +0 -1
- package/dist/__tests__/init-template-portability.test.js +0 -1
- package/dist/__tests__/init.test.js +249 -0
- package/dist/__tests__/initiative-e2e.test.js +442 -0
- package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
- package/dist/__tests__/memory-integration.test.js +333 -0
- package/dist/__tests__/release.test.js +1 -1
- package/dist/__tests__/safe-git.test.js +0 -1
- package/dist/__tests__/state-doctor.test.js +54 -0
- package/dist/__tests__/sync-templates.test.js +255 -0
- package/dist/__tests__/wu-create-required-fields.test.js +121 -0
- package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
- package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
- package/dist/backlog-prune.js +0 -1
- package/dist/cli-entry-point.js +0 -1
- package/dist/commands/integrate.js +229 -0
- package/dist/commands.js +171 -0
- package/dist/docs-sync.js +46 -0
- package/dist/doctor.js +479 -10
- package/dist/gates.js +0 -7
- package/dist/hooks/enforcement-checks.js +209 -0
- package/dist/hooks/enforcement-generator.js +365 -0
- package/dist/hooks/enforcement-sync.js +243 -0
- package/dist/hooks/index.js +7 -0
- package/dist/init.js +502 -17
- package/dist/initiative-add-wu.js +0 -2
- package/dist/initiative-create.js +0 -3
- package/dist/initiative-edit.js +0 -5
- package/dist/initiative-plan.js +0 -1
- package/dist/initiative-remove-wu.js +0 -2
- package/dist/lane-health.js +0 -2
- package/dist/lane-suggest.js +0 -1
- package/dist/mem-checkpoint.js +0 -2
- package/dist/mem-cleanup.js +0 -2
- package/dist/mem-context.js +0 -3
- package/dist/mem-create.js +0 -2
- package/dist/mem-delete.js +0 -3
- package/dist/mem-inbox.js +0 -2
- package/dist/mem-index.js +0 -1
- package/dist/mem-init.js +0 -2
- package/dist/mem-profile.js +0 -1
- package/dist/mem-promote.js +0 -1
- package/dist/mem-ready.js +0 -2
- package/dist/mem-signal.js +0 -2
- package/dist/mem-start.js +0 -2
- package/dist/mem-summarize.js +0 -2
- package/dist/metrics-cli.js +1 -1
- package/dist/metrics-snapshot.js +1 -1
- package/dist/onboarding-smoke-test.js +0 -5
- package/dist/orchestrate-init-status.js +0 -1
- package/dist/orchestrate-initiative.js +0 -1
- package/dist/orchestrate-monitor.js +0 -1
- package/dist/plan-create.js +0 -2
- package/dist/plan-edit.js +0 -2
- package/dist/plan-link.js +0 -2
- package/dist/plan-promote.js +0 -2
- package/dist/signal-cleanup.js +0 -4
- package/dist/state-bootstrap.js +0 -1
- package/dist/state-cleanup.js +0 -4
- package/dist/state-doctor-fix.js +5 -8
- package/dist/state-doctor.js +0 -11
- package/dist/sync-templates.js +188 -34
- package/dist/wu-block.js +100 -48
- package/dist/wu-claim.js +1 -22
- package/dist/wu-cleanup.js +0 -1
- package/dist/wu-create.js +0 -2
- package/dist/wu-done-auto-cleanup.js +139 -0
- package/dist/wu-done.js +11 -4
- package/dist/wu-edit.js +0 -12
- package/dist/wu-preflight.js +0 -1
- package/dist/wu-prep.js +0 -1
- package/dist/wu-proto.js +0 -1
- package/dist/wu-spawn.js +0 -3
- package/dist/wu-unblock.js +0 -2
- package/dist/wu-validate.js +0 -1
- package/package.json +9 -7
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Spawn Coordination Integration Tests (WU-1363)
|
|
3
|
+
*
|
|
4
|
+
* Integration tests for agent spawn coordination:
|
|
5
|
+
* - AC4: Agent spawn coordination
|
|
6
|
+
*
|
|
7
|
+
* These tests validate the spawn system's ability to:
|
|
8
|
+
* - Generate spawn prompts for WUs
|
|
9
|
+
* - Check lane occupation before spawning
|
|
10
|
+
* - Record spawn events to registry
|
|
11
|
+
* - Coordinate parallel agents via signals
|
|
12
|
+
*
|
|
13
|
+
* TDD: Tests written BEFORE implementation verification.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
16
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { execFileSync } from 'node:child_process';
|
|
20
|
+
import { stringifyYAML, parseYAML } from '@lumenflow/core/dist/wu-yaml.js';
|
|
21
|
+
import { WU_STATUS } from '@lumenflow/core/dist/wu-constants.js';
|
|
22
|
+
import { generateTaskInvocation, generateActionSection, checkLaneOccupation, generateLaneOccupationWarning, generateEffortScalingRules, generateParallelToolCallGuidance, generateCompletionFormat, } from '../wu-spawn.js';
|
|
23
|
+
import { SpawnStrategyFactory } from '@lumenflow/core/dist/spawn-strategy.js';
|
|
24
|
+
import { createSignal, loadSignals } from '@lumenflow/memory';
|
|
25
|
+
// Test constants
|
|
26
|
+
const TEST_WU_ID = 'WU-9920';
|
|
27
|
+
const TEST_LANE = 'Framework: CLI';
|
|
28
|
+
const TEST_TITLE = 'Spawn coordination test';
|
|
29
|
+
const TEST_DESCRIPTION = 'Context: Testing spawn. Problem: Need coordination. Solution: Use signals.';
|
|
30
|
+
/**
|
|
31
|
+
* Helper to create a test project with spawn infrastructure
|
|
32
|
+
*/
|
|
33
|
+
function createSpawnProject(baseDir) {
|
|
34
|
+
const dirs = [
|
|
35
|
+
'docs/04-operations/tasks/wu',
|
|
36
|
+
'.lumenflow/state',
|
|
37
|
+
'.lumenflow/memory',
|
|
38
|
+
'.lumenflow/stamps',
|
|
39
|
+
'.lumenflow/locks',
|
|
40
|
+
'packages/@lumenflow/cli/src',
|
|
41
|
+
];
|
|
42
|
+
for (const dir of dirs) {
|
|
43
|
+
mkdirSync(join(baseDir, dir), { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
// Create config with lane definitions
|
|
46
|
+
const configContent = `
|
|
47
|
+
version: 1
|
|
48
|
+
lanes:
|
|
49
|
+
definitions:
|
|
50
|
+
- name: 'Framework: CLI'
|
|
51
|
+
wip_limit: 1
|
|
52
|
+
code_paths:
|
|
53
|
+
- 'packages/@lumenflow/cli/**'
|
|
54
|
+
- name: 'Framework: Core'
|
|
55
|
+
wip_limit: 1
|
|
56
|
+
code_paths:
|
|
57
|
+
- 'packages/@lumenflow/core/**'
|
|
58
|
+
agents:
|
|
59
|
+
defaultClient: claude-code
|
|
60
|
+
git:
|
|
61
|
+
requireRemote: false
|
|
62
|
+
`;
|
|
63
|
+
writeFileSync(join(baseDir, '.lumenflow.config.yaml'), configContent);
|
|
64
|
+
// Initialize git
|
|
65
|
+
execFileSync('git', ['init'], { cwd: baseDir, stdio: 'pipe' });
|
|
66
|
+
execFileSync('git', ['config', 'user.email', 'test@test.com'], { cwd: baseDir, stdio: 'pipe' });
|
|
67
|
+
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: baseDir, stdio: 'pipe' });
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Helper to create a WU for spawn testing
|
|
71
|
+
*/
|
|
72
|
+
function createSpawnWU(baseDir, id, options = {}) {
|
|
73
|
+
const wuDir = join(baseDir, 'docs/04-operations/tasks/wu');
|
|
74
|
+
const wuPath = join(wuDir, `${id}.yaml`);
|
|
75
|
+
const doc = {
|
|
76
|
+
id,
|
|
77
|
+
title: TEST_TITLE,
|
|
78
|
+
lane: options.lane || TEST_LANE,
|
|
79
|
+
status: options.status || WU_STATUS.READY,
|
|
80
|
+
type: 'feature',
|
|
81
|
+
priority: 'P2',
|
|
82
|
+
created: '2026-02-03',
|
|
83
|
+
description: TEST_DESCRIPTION,
|
|
84
|
+
acceptance: ['Spawn works correctly', 'Signals are sent'],
|
|
85
|
+
code_paths: ['packages/@lumenflow/cli/src'],
|
|
86
|
+
tests: {
|
|
87
|
+
unit: ['packages/@lumenflow/cli/src/__tests__/spawn.test.ts'],
|
|
88
|
+
},
|
|
89
|
+
exposure: 'backend-only',
|
|
90
|
+
};
|
|
91
|
+
if (options.worktreePath) {
|
|
92
|
+
doc.worktree_path = options.worktreePath;
|
|
93
|
+
}
|
|
94
|
+
if (options.claimedAt) {
|
|
95
|
+
doc.claimed_at = options.claimedAt;
|
|
96
|
+
}
|
|
97
|
+
writeFileSync(wuPath, stringifyYAML(doc));
|
|
98
|
+
return wuPath;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Helper to create a lane lock
|
|
102
|
+
*/
|
|
103
|
+
function createLaneLock(baseDir, lane, wuId) {
|
|
104
|
+
const lockDir = join(baseDir, '.lumenflow/locks');
|
|
105
|
+
mkdirSync(lockDir, { recursive: true });
|
|
106
|
+
const laneSlug = lane.toLowerCase().replace(/[:\s]+/g, '-');
|
|
107
|
+
const lockPath = join(lockDir, `${laneSlug}.lock`);
|
|
108
|
+
const lockContent = {
|
|
109
|
+
lane,
|
|
110
|
+
wuId,
|
|
111
|
+
lockedAt: new Date().toISOString(),
|
|
112
|
+
agent: 'test-agent',
|
|
113
|
+
};
|
|
114
|
+
writeFileSync(lockPath, JSON.stringify(lockContent, null, 2));
|
|
115
|
+
}
|
|
116
|
+
describe('Agent Spawn Coordination Integration Tests (WU-1363)', () => {
|
|
117
|
+
let tempDir;
|
|
118
|
+
let originalCwd;
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
tempDir = join(tmpdir(), `spawn-coordination-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
121
|
+
mkdirSync(tempDir, { recursive: true });
|
|
122
|
+
originalCwd = process.cwd();
|
|
123
|
+
createSpawnProject(tempDir);
|
|
124
|
+
vi.resetModules();
|
|
125
|
+
});
|
|
126
|
+
afterEach(() => {
|
|
127
|
+
process.chdir(originalCwd);
|
|
128
|
+
if (existsSync(tempDir)) {
|
|
129
|
+
try {
|
|
130
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Ignore cleanup errors
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
vi.clearAllMocks();
|
|
137
|
+
});
|
|
138
|
+
describe('AC4: Integration tests for agent spawn coordination', () => {
|
|
139
|
+
describe('spawn prompt generation', () => {
|
|
140
|
+
it('should generate Task tool invocation with correct structure', () => {
|
|
141
|
+
// Arrange
|
|
142
|
+
const doc = {
|
|
143
|
+
title: TEST_TITLE,
|
|
144
|
+
lane: TEST_LANE,
|
|
145
|
+
status: WU_STATUS.IN_PROGRESS,
|
|
146
|
+
type: 'feature',
|
|
147
|
+
description: TEST_DESCRIPTION,
|
|
148
|
+
code_paths: ['packages/@lumenflow/cli/src'],
|
|
149
|
+
acceptance: ['Test criterion'],
|
|
150
|
+
worktree_path: 'worktrees/framework-cli-wu-9920',
|
|
151
|
+
};
|
|
152
|
+
const strategy = SpawnStrategyFactory.create('claude-code');
|
|
153
|
+
// Act
|
|
154
|
+
const invocation = generateTaskInvocation(doc, TEST_WU_ID, strategy);
|
|
155
|
+
// Assert
|
|
156
|
+
expect(invocation).toContain('antml:invoke');
|
|
157
|
+
expect(invocation).toContain('antml:function_calls');
|
|
158
|
+
expect(invocation).toContain(TEST_WU_ID);
|
|
159
|
+
expect(invocation).toContain('general-purpose');
|
|
160
|
+
});
|
|
161
|
+
it('should include WU details in spawn prompt', () => {
|
|
162
|
+
// Arrange
|
|
163
|
+
const doc = {
|
|
164
|
+
title: TEST_TITLE,
|
|
165
|
+
lane: TEST_LANE,
|
|
166
|
+
status: WU_STATUS.IN_PROGRESS,
|
|
167
|
+
type: 'feature',
|
|
168
|
+
description: TEST_DESCRIPTION,
|
|
169
|
+
code_paths: ['packages/@lumenflow/cli/src'],
|
|
170
|
+
acceptance: ['Criterion 1', 'Criterion 2'],
|
|
171
|
+
};
|
|
172
|
+
const strategy = SpawnStrategyFactory.create('claude-code');
|
|
173
|
+
// Act
|
|
174
|
+
const invocation = generateTaskInvocation(doc, TEST_WU_ID, strategy);
|
|
175
|
+
// Assert
|
|
176
|
+
expect(invocation).toContain(TEST_TITLE);
|
|
177
|
+
expect(invocation).toContain(TEST_LANE);
|
|
178
|
+
expect(invocation).toContain('Criterion 1');
|
|
179
|
+
expect(invocation).toContain('Criterion 2');
|
|
180
|
+
});
|
|
181
|
+
it('should include constraints block at end', () => {
|
|
182
|
+
// Arrange
|
|
183
|
+
const doc = {
|
|
184
|
+
title: TEST_TITLE,
|
|
185
|
+
lane: TEST_LANE,
|
|
186
|
+
status: WU_STATUS.READY,
|
|
187
|
+
type: 'feature',
|
|
188
|
+
description: TEST_DESCRIPTION,
|
|
189
|
+
code_paths: [],
|
|
190
|
+
acceptance: [],
|
|
191
|
+
};
|
|
192
|
+
const strategy = SpawnStrategyFactory.create('claude-code');
|
|
193
|
+
// Act
|
|
194
|
+
const invocation = generateTaskInvocation(doc, TEST_WU_ID, strategy);
|
|
195
|
+
// Assert
|
|
196
|
+
expect(invocation).toContain('<constraints>');
|
|
197
|
+
expect(invocation).toContain('CRITICAL RULES');
|
|
198
|
+
expect(invocation).toContain('LUMENFLOW_SPAWN_END');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
describe('action section generation', () => {
|
|
202
|
+
it('should instruct to claim when WU is unclaimed', () => {
|
|
203
|
+
// Arrange
|
|
204
|
+
const doc = {
|
|
205
|
+
lane: TEST_LANE,
|
|
206
|
+
status: WU_STATUS.READY,
|
|
207
|
+
};
|
|
208
|
+
// Act
|
|
209
|
+
const action = generateActionSection(doc, TEST_WU_ID);
|
|
210
|
+
// Assert
|
|
211
|
+
expect(action).toContain('wu:claim');
|
|
212
|
+
expect(action).toContain('FIRST');
|
|
213
|
+
expect(action).toContain(TEST_WU_ID);
|
|
214
|
+
});
|
|
215
|
+
it('should instruct to continue when WU is already claimed', () => {
|
|
216
|
+
// Arrange
|
|
217
|
+
const doc = {
|
|
218
|
+
lane: TEST_LANE,
|
|
219
|
+
status: WU_STATUS.IN_PROGRESS,
|
|
220
|
+
claimed_at: new Date().toISOString(),
|
|
221
|
+
worktree_path: 'worktrees/framework-cli-wu-9920',
|
|
222
|
+
};
|
|
223
|
+
// Act
|
|
224
|
+
const action = generateActionSection(doc, TEST_WU_ID);
|
|
225
|
+
// Assert
|
|
226
|
+
expect(action).toContain('already claimed');
|
|
227
|
+
expect(action).toContain('worktrees/framework-cli-wu-9920');
|
|
228
|
+
expect(action).not.toContain('wu:claim');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
describe('lane occupation checking', () => {
|
|
232
|
+
it('should detect when lane is occupied by another WU', () => {
|
|
233
|
+
// Arrange
|
|
234
|
+
process.chdir(tempDir);
|
|
235
|
+
createLaneLock(tempDir, TEST_LANE, 'WU-8888');
|
|
236
|
+
// Act
|
|
237
|
+
const occupation = checkLaneOccupation(TEST_LANE);
|
|
238
|
+
// Assert
|
|
239
|
+
// Note: This may return null in test environment without full state
|
|
240
|
+
// The important thing is the function runs without error
|
|
241
|
+
expect(typeof occupation).toBe('object');
|
|
242
|
+
});
|
|
243
|
+
it('should generate occupation warning message', () => {
|
|
244
|
+
// Arrange
|
|
245
|
+
const lockMetadata = {
|
|
246
|
+
lane: TEST_LANE,
|
|
247
|
+
wuId: 'WU-8888',
|
|
248
|
+
};
|
|
249
|
+
// Act
|
|
250
|
+
const warning = generateLaneOccupationWarning(lockMetadata, TEST_WU_ID);
|
|
251
|
+
// Assert
|
|
252
|
+
expect(warning).toContain(TEST_LANE);
|
|
253
|
+
expect(warning).toContain('WU-8888');
|
|
254
|
+
expect(warning).toContain('Options');
|
|
255
|
+
expect(warning).toContain('WIP=');
|
|
256
|
+
});
|
|
257
|
+
it('should include stale lock guidance when lock is old', () => {
|
|
258
|
+
// Arrange
|
|
259
|
+
const lockMetadata = {
|
|
260
|
+
lane: TEST_LANE,
|
|
261
|
+
wuId: 'WU-8888',
|
|
262
|
+
};
|
|
263
|
+
// Act
|
|
264
|
+
const warning = generateLaneOccupationWarning(lockMetadata, TEST_WU_ID, { isStale: true });
|
|
265
|
+
// Assert
|
|
266
|
+
expect(warning).toContain('STALE');
|
|
267
|
+
expect(warning).toContain('wu:block');
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
describe('effort scaling rules', () => {
|
|
271
|
+
it('should include complexity heuristics', () => {
|
|
272
|
+
// Act
|
|
273
|
+
const rules = generateEffortScalingRules();
|
|
274
|
+
// Assert
|
|
275
|
+
expect(rules).toContain('Simple');
|
|
276
|
+
expect(rules).toContain('Moderate');
|
|
277
|
+
expect(rules).toContain('Complex');
|
|
278
|
+
expect(rules).toContain('Multi-domain');
|
|
279
|
+
expect(rules).toContain('Tool Calls');
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
describe('parallel tool call guidance', () => {
|
|
283
|
+
it('should include parallelism instructions', () => {
|
|
284
|
+
// Act
|
|
285
|
+
const guidance = generateParallelToolCallGuidance();
|
|
286
|
+
// Assert
|
|
287
|
+
expect(guidance).toContain('parallel');
|
|
288
|
+
expect(guidance).toContain('independent');
|
|
289
|
+
expect(guidance).toContain('Good examples');
|
|
290
|
+
expect(guidance).toContain('Bad examples');
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
describe('completion format', () => {
|
|
294
|
+
it('should include structured output format', () => {
|
|
295
|
+
// Act
|
|
296
|
+
const format = generateCompletionFormat(TEST_WU_ID);
|
|
297
|
+
// Assert
|
|
298
|
+
expect(format).toContain('Summary');
|
|
299
|
+
expect(format).toContain('Artifacts');
|
|
300
|
+
expect(format).toContain('Verification');
|
|
301
|
+
expect(format).toContain('Blockers');
|
|
302
|
+
expect(format).toContain('Follow-up');
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
describe('signal-based coordination', () => {
|
|
306
|
+
it('should allow agents to signal progress', async () => {
|
|
307
|
+
// Arrange
|
|
308
|
+
process.chdir(tempDir);
|
|
309
|
+
// Act - Agent sends progress signal
|
|
310
|
+
const result = await createSignal(tempDir, {
|
|
311
|
+
message: 'AC1 complete: tests passing',
|
|
312
|
+
wuId: TEST_WU_ID,
|
|
313
|
+
lane: TEST_LANE,
|
|
314
|
+
});
|
|
315
|
+
// Assert
|
|
316
|
+
expect(result.success).toBe(true);
|
|
317
|
+
expect(result.signal.wu_id).toBe(TEST_WU_ID);
|
|
318
|
+
});
|
|
319
|
+
it('should allow agents to check for signals from other agents', async () => {
|
|
320
|
+
// Arrange
|
|
321
|
+
process.chdir(tempDir);
|
|
322
|
+
// Another agent sends a signal
|
|
323
|
+
await createSignal(tempDir, {
|
|
324
|
+
message: 'Dependency WU-8888 complete',
|
|
325
|
+
wuId: 'WU-8888',
|
|
326
|
+
lane: TEST_LANE,
|
|
327
|
+
});
|
|
328
|
+
// Current agent's signal
|
|
329
|
+
await createSignal(tempDir, {
|
|
330
|
+
message: 'Starting WU-9920',
|
|
331
|
+
wuId: TEST_WU_ID,
|
|
332
|
+
lane: TEST_LANE,
|
|
333
|
+
});
|
|
334
|
+
// Act - Check lane signals (from any agent in the lane)
|
|
335
|
+
const laneSignals = await loadSignals(tempDir, { lane: TEST_LANE });
|
|
336
|
+
// Assert
|
|
337
|
+
expect(laneSignals).toHaveLength(2);
|
|
338
|
+
});
|
|
339
|
+
it('should support WU-specific signal filtering', async () => {
|
|
340
|
+
// Arrange
|
|
341
|
+
process.chdir(tempDir);
|
|
342
|
+
await createSignal(tempDir, { message: 'Signal 1', wuId: TEST_WU_ID });
|
|
343
|
+
await createSignal(tempDir, { message: 'Signal 2', wuId: TEST_WU_ID });
|
|
344
|
+
await createSignal(tempDir, { message: 'Other WU', wuId: 'WU-8888' });
|
|
345
|
+
// Act
|
|
346
|
+
const wuSignals = await loadSignals(tempDir, { wuId: TEST_WU_ID });
|
|
347
|
+
// Assert
|
|
348
|
+
expect(wuSignals).toHaveLength(2);
|
|
349
|
+
wuSignals.forEach((sig) => {
|
|
350
|
+
expect(sig.wu_id).toBe(TEST_WU_ID);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
describe('spawn registry', () => {
|
|
355
|
+
it('should record spawn events', async () => {
|
|
356
|
+
// Arrange
|
|
357
|
+
process.chdir(tempDir);
|
|
358
|
+
const registryPath = join(tempDir, '.lumenflow/state/spawn-registry.jsonl');
|
|
359
|
+
// Act - Record spawn event directly
|
|
360
|
+
const spawnEvent = {
|
|
361
|
+
id: 'spawn-12345',
|
|
362
|
+
parentWuId: 'WU-1363',
|
|
363
|
+
targetWuId: TEST_WU_ID,
|
|
364
|
+
lane: TEST_LANE,
|
|
365
|
+
spawnedAt: new Date().toISOString(),
|
|
366
|
+
};
|
|
367
|
+
writeFileSync(registryPath, JSON.stringify(spawnEvent) + '\n');
|
|
368
|
+
// Assert
|
|
369
|
+
expect(existsSync(registryPath)).toBe(true);
|
|
370
|
+
const content = readFileSync(registryPath, 'utf-8');
|
|
371
|
+
expect(content).toContain(TEST_WU_ID);
|
|
372
|
+
expect(content).toContain('WU-1363');
|
|
373
|
+
});
|
|
374
|
+
it('should track multiple spawn events', async () => {
|
|
375
|
+
// Arrange
|
|
376
|
+
process.chdir(tempDir);
|
|
377
|
+
const registryPath = join(tempDir, '.lumenflow/state/spawn-registry.jsonl');
|
|
378
|
+
// Act - Record multiple spawn events
|
|
379
|
+
const events = [
|
|
380
|
+
{ id: 'spawn-1', targetWuId: 'WU-001', lane: 'Framework: CLI' },
|
|
381
|
+
{ id: 'spawn-2', targetWuId: 'WU-002', lane: 'Framework: Core' },
|
|
382
|
+
{ id: 'spawn-3', targetWuId: 'WU-003', lane: 'Framework: CLI' },
|
|
383
|
+
];
|
|
384
|
+
const content = events.map((e) => JSON.stringify(e)).join('\n') + '\n';
|
|
385
|
+
writeFileSync(registryPath, content);
|
|
386
|
+
// Assert
|
|
387
|
+
const lines = readFileSync(registryPath, 'utf-8').trim().split('\n');
|
|
388
|
+
expect(lines).toHaveLength(3);
|
|
389
|
+
const parsed = lines.map((line) => JSON.parse(line));
|
|
390
|
+
expect(parsed.map((e) => e.targetWuId)).toEqual(['WU-001', 'WU-002', 'WU-003']);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
describe('complete spawn coordination workflow', () => {
|
|
394
|
+
it('should support full spawn and signal workflow', async () => {
|
|
395
|
+
// This test validates the complete spawn coordination:
|
|
396
|
+
// 1. Generate spawn prompt for WU
|
|
397
|
+
// 2. Record spawn event
|
|
398
|
+
// 3. Spawned agent sends signals
|
|
399
|
+
// 4. Parent agent receives signals
|
|
400
|
+
// 5. Spawned agent completes
|
|
401
|
+
// Arrange
|
|
402
|
+
process.chdir(tempDir);
|
|
403
|
+
createSpawnWU(tempDir, TEST_WU_ID, {
|
|
404
|
+
status: WU_STATUS.READY,
|
|
405
|
+
lane: TEST_LANE,
|
|
406
|
+
});
|
|
407
|
+
// Step 1: Generate spawn prompt
|
|
408
|
+
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
409
|
+
const doc = parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
410
|
+
const strategy = SpawnStrategyFactory.create('claude-code');
|
|
411
|
+
const invocation = generateTaskInvocation(doc, TEST_WU_ID, strategy);
|
|
412
|
+
expect(invocation).toContain(TEST_WU_ID);
|
|
413
|
+
expect(invocation).toContain('antml:invoke');
|
|
414
|
+
// Step 2: Record spawn event
|
|
415
|
+
const registryPath = join(tempDir, '.lumenflow/state/spawn-registry.jsonl');
|
|
416
|
+
const spawnEvent = {
|
|
417
|
+
id: 'spawn-test-001',
|
|
418
|
+
parentWuId: 'WU-1363',
|
|
419
|
+
targetWuId: TEST_WU_ID,
|
|
420
|
+
lane: TEST_LANE,
|
|
421
|
+
spawnedAt: new Date().toISOString(),
|
|
422
|
+
};
|
|
423
|
+
writeFileSync(registryPath, JSON.stringify(spawnEvent) + '\n');
|
|
424
|
+
expect(existsSync(registryPath)).toBe(true);
|
|
425
|
+
// Step 3: Spawned agent sends progress signals
|
|
426
|
+
await createSignal(tempDir, {
|
|
427
|
+
message: 'Starting implementation',
|
|
428
|
+
wuId: TEST_WU_ID,
|
|
429
|
+
lane: TEST_LANE,
|
|
430
|
+
});
|
|
431
|
+
await createSignal(tempDir, {
|
|
432
|
+
message: 'AC1 complete',
|
|
433
|
+
wuId: TEST_WU_ID,
|
|
434
|
+
lane: TEST_LANE,
|
|
435
|
+
});
|
|
436
|
+
// Step 4: Parent agent checks signals
|
|
437
|
+
const signals = await loadSignals(tempDir, { wuId: TEST_WU_ID });
|
|
438
|
+
expect(signals).toHaveLength(2);
|
|
439
|
+
// Step 5: Spawned agent sends completion signal
|
|
440
|
+
await createSignal(tempDir, {
|
|
441
|
+
message: 'All ACs complete, running gates',
|
|
442
|
+
wuId: TEST_WU_ID,
|
|
443
|
+
lane: TEST_LANE,
|
|
444
|
+
});
|
|
445
|
+
const allSignals = await loadSignals(tempDir, { wuId: TEST_WU_ID });
|
|
446
|
+
expect(allSignals).toHaveLength(3);
|
|
447
|
+
expect(allSignals[2].message).toContain('complete');
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file integrate.test.ts
|
|
3
|
+
* Tests for Claude Code integration command (WU-1367)
|
|
4
|
+
*/
|
|
5
|
+
// Test file lint exceptions
|
|
6
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
// Mock fs
|
|
9
|
+
vi.mock('node:fs');
|
|
10
|
+
const TEST_PROJECT_DIR = '/test/project';
|
|
11
|
+
describe('WU-1367: Integrate Command', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.resetAllMocks();
|
|
14
|
+
vi.mocked(fs.mkdirSync).mockReturnValue(undefined);
|
|
15
|
+
});
|
|
16
|
+
describe('integrateClaudeCode', () => {
|
|
17
|
+
it('should skip integration when enforcement not enabled', async () => {
|
|
18
|
+
const mockWriteFileSync = vi.mocked(fs.writeFileSync);
|
|
19
|
+
mockWriteFileSync.mockClear();
|
|
20
|
+
const { integrateClaudeCode } = await import('../../commands/integrate.js');
|
|
21
|
+
await integrateClaudeCode(TEST_PROJECT_DIR, {
|
|
22
|
+
enforcement: {
|
|
23
|
+
hooks: false,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
// Should not write any files
|
|
27
|
+
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
|
28
|
+
});
|
|
29
|
+
it('should create hooks directory when it does not exist', async () => {
|
|
30
|
+
const mockMkdirSync = vi.mocked(fs.mkdirSync);
|
|
31
|
+
vi.mocked(fs.writeFileSync);
|
|
32
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
33
|
+
vi.mocked(fs.readFileSync).mockReturnValue('{}');
|
|
34
|
+
const { integrateClaudeCode } = await import('../../commands/integrate.js');
|
|
35
|
+
await integrateClaudeCode(TEST_PROJECT_DIR, {
|
|
36
|
+
enforcement: {
|
|
37
|
+
hooks: true,
|
|
38
|
+
block_outside_worktree: true,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
expect(mockMkdirSync).toHaveBeenCalledWith(expect.stringContaining('.claude/hooks'), {
|
|
42
|
+
recursive: true,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
it('should generate enforce-worktree.sh when block_outside_worktree=true', async () => {
|
|
46
|
+
const mockWriteFileSync = vi.mocked(fs.writeFileSync);
|
|
47
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
48
|
+
vi.mocked(fs.readFileSync).mockReturnValue('{}');
|
|
49
|
+
mockWriteFileSync.mockClear();
|
|
50
|
+
const { integrateClaudeCode } = await import('../../commands/integrate.js');
|
|
51
|
+
await integrateClaudeCode(TEST_PROJECT_DIR, {
|
|
52
|
+
enforcement: {
|
|
53
|
+
hooks: true,
|
|
54
|
+
block_outside_worktree: true,
|
|
55
|
+
require_wu_for_edits: false,
|
|
56
|
+
warn_on_stop_without_wu_done: false,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const enforceWorktreeCall = mockWriteFileSync.mock.calls.find((call) => String(call[0]).includes('enforce-worktree.sh'));
|
|
60
|
+
expect(enforceWorktreeCall).toBeDefined();
|
|
61
|
+
expect(enforceWorktreeCall[1]).toContain('enforce-worktree.sh');
|
|
62
|
+
});
|
|
63
|
+
it('should generate require-wu.sh when require_wu_for_edits=true', async () => {
|
|
64
|
+
const mockWriteFileSync = vi.mocked(fs.writeFileSync);
|
|
65
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
66
|
+
vi.mocked(fs.readFileSync).mockReturnValue('{}');
|
|
67
|
+
mockWriteFileSync.mockClear();
|
|
68
|
+
const { integrateClaudeCode } = await import('../../commands/integrate.js');
|
|
69
|
+
await integrateClaudeCode(TEST_PROJECT_DIR, {
|
|
70
|
+
enforcement: {
|
|
71
|
+
hooks: true,
|
|
72
|
+
block_outside_worktree: false,
|
|
73
|
+
require_wu_for_edits: true,
|
|
74
|
+
warn_on_stop_without_wu_done: false,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const requireWuCall = mockWriteFileSync.mock.calls.find((call) => String(call[0]).includes('require-wu.sh'));
|
|
78
|
+
expect(requireWuCall).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
it('should generate warn-incomplete.sh when warn_on_stop_without_wu_done=true', async () => {
|
|
81
|
+
const mockWriteFileSync = vi.mocked(fs.writeFileSync);
|
|
82
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
83
|
+
vi.mocked(fs.readFileSync).mockReturnValue('{}');
|
|
84
|
+
mockWriteFileSync.mockClear();
|
|
85
|
+
const { integrateClaudeCode } = await import('../../commands/integrate.js');
|
|
86
|
+
await integrateClaudeCode(TEST_PROJECT_DIR, {
|
|
87
|
+
enforcement: {
|
|
88
|
+
hooks: true,
|
|
89
|
+
block_outside_worktree: false,
|
|
90
|
+
require_wu_for_edits: false,
|
|
91
|
+
warn_on_stop_without_wu_done: true,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const warnIncompleteCall = mockWriteFileSync.mock.calls.find((call) => String(call[0]).includes('warn-incomplete.sh'));
|
|
95
|
+
expect(warnIncompleteCall).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
it('should update settings.json with PreToolUse hooks', async () => {
|
|
98
|
+
const mockWriteFileSync = vi.mocked(fs.writeFileSync);
|
|
99
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
100
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
|
101
|
+
$schema: 'https://json.schemastore.org/claude-code-settings.json',
|
|
102
|
+
permissions: { allow: ['Bash'] },
|
|
103
|
+
}));
|
|
104
|
+
mockWriteFileSync.mockClear();
|
|
105
|
+
const { integrateClaudeCode } = await import('../../commands/integrate.js');
|
|
106
|
+
await integrateClaudeCode(TEST_PROJECT_DIR, {
|
|
107
|
+
enforcement: {
|
|
108
|
+
hooks: true,
|
|
109
|
+
block_outside_worktree: true,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const settingsCall = mockWriteFileSync.mock.calls.find((call) => String(call[0]).includes('settings.json'));
|
|
113
|
+
expect(settingsCall).toBeDefined();
|
|
114
|
+
const settingsContent = JSON.parse(settingsCall[1]);
|
|
115
|
+
expect(settingsContent.hooks).toBeDefined();
|
|
116
|
+
expect(settingsContent.hooks.PreToolUse).toBeDefined();
|
|
117
|
+
expect(settingsContent.hooks.PreToolUse[0].matcher).toBe('Write|Edit');
|
|
118
|
+
});
|
|
119
|
+
it('should update settings.json with Stop hooks', async () => {
|
|
120
|
+
const mockWriteFileSync = vi.mocked(fs.writeFileSync);
|
|
121
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
122
|
+
vi.mocked(fs.readFileSync).mockReturnValue('{}');
|
|
123
|
+
mockWriteFileSync.mockClear();
|
|
124
|
+
const { integrateClaudeCode } = await import('../../commands/integrate.js');
|
|
125
|
+
await integrateClaudeCode(TEST_PROJECT_DIR, {
|
|
126
|
+
enforcement: {
|
|
127
|
+
hooks: true,
|
|
128
|
+
block_outside_worktree: false,
|
|
129
|
+
require_wu_for_edits: false,
|
|
130
|
+
warn_on_stop_without_wu_done: true,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const settingsCall = mockWriteFileSync.mock.calls.find((call) => String(call[0]).includes('settings.json'));
|
|
134
|
+
expect(settingsCall).toBeDefined();
|
|
135
|
+
const settingsContent = JSON.parse(settingsCall[1]);
|
|
136
|
+
expect(settingsContent.hooks).toBeDefined();
|
|
137
|
+
expect(settingsContent.hooks.Stop).toBeDefined();
|
|
138
|
+
});
|
|
139
|
+
it('should preserve existing permissions when updating settings.json', async () => {
|
|
140
|
+
const mockWriteFileSync = vi.mocked(fs.writeFileSync);
|
|
141
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
142
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
|
143
|
+
$schema: 'https://json.schemastore.org/claude-code-settings.json',
|
|
144
|
+
permissions: {
|
|
145
|
+
allow: ['Bash', 'Read', 'Write'],
|
|
146
|
+
deny: ['Bash(rm -rf /*)'],
|
|
147
|
+
},
|
|
148
|
+
}));
|
|
149
|
+
mockWriteFileSync.mockClear();
|
|
150
|
+
const { integrateClaudeCode } = await import('../../commands/integrate.js');
|
|
151
|
+
await integrateClaudeCode(TEST_PROJECT_DIR, {
|
|
152
|
+
enforcement: {
|
|
153
|
+
hooks: true,
|
|
154
|
+
block_outside_worktree: true,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
const settingsCall = mockWriteFileSync.mock.calls.find((call) => String(call[0]).includes('settings.json'));
|
|
158
|
+
expect(settingsCall).toBeDefined();
|
|
159
|
+
const settingsContent = JSON.parse(settingsCall[1]);
|
|
160
|
+
expect(settingsContent.permissions).toBeDefined();
|
|
161
|
+
expect(settingsContent.permissions.allow).toContain('Bash');
|
|
162
|
+
expect(settingsContent.permissions.deny).toContain('Bash(rm -rf /*)');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|