@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,388 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WU Lifecycle Integration Tests (WU-1363)
|
|
3
|
-
*
|
|
4
|
-
* Integration tests covering the full WU lifecycle:
|
|
5
|
-
* - AC1: wu:create, wu:claim, wu:status
|
|
6
|
-
* - AC2: wu:prep, wu:done workflow
|
|
7
|
-
*
|
|
8
|
-
* These tests validate the end-to-end behavior of WU lifecycle commands
|
|
9
|
-
* by running them in isolated temporary directories with proper git setup.
|
|
10
|
-
*
|
|
11
|
-
* TDD: Tests written BEFORE implementation verification.
|
|
12
|
-
*/
|
|
13
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
14
|
-
import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
-
import { join } from 'node:path';
|
|
16
|
-
import { tmpdir } from 'node:os';
|
|
17
|
-
import { execFileSync } from 'node:child_process';
|
|
18
|
-
import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
|
|
19
|
-
import { WU_STATUS } from '@lumenflow/core/dist/wu-constants.js';
|
|
20
|
-
// Test constants
|
|
21
|
-
const TEST_WU_ID = 'WU-9901';
|
|
22
|
-
const TEST_LANE = 'Framework: CLI';
|
|
23
|
-
const TEST_TITLE = 'Integration test WU';
|
|
24
|
-
const TEST_DESCRIPTION = 'Context: Integration test. Problem: Need to test lifecycle. Solution: Run integration tests.';
|
|
25
|
-
/**
|
|
26
|
-
* Helper to create a minimal LumenFlow project structure
|
|
27
|
-
*/
|
|
28
|
-
function createTestProject(baseDir) {
|
|
29
|
-
// Create directory structure
|
|
30
|
-
const dirs = [
|
|
31
|
-
'docs/04-operations/tasks/wu',
|
|
32
|
-
'docs/04-operations/tasks/initiatives',
|
|
33
|
-
'.lumenflow/state',
|
|
34
|
-
'.lumenflow/stamps',
|
|
35
|
-
'packages/@lumenflow/cli/src/__tests__',
|
|
36
|
-
];
|
|
37
|
-
for (const dir of dirs) {
|
|
38
|
-
mkdirSync(join(baseDir, dir), { recursive: true });
|
|
39
|
-
}
|
|
40
|
-
// Create minimal .lumenflow.config.yaml
|
|
41
|
-
const configContent = `
|
|
42
|
-
version: 1
|
|
43
|
-
lanes:
|
|
44
|
-
definitions:
|
|
45
|
-
- name: 'Framework: CLI'
|
|
46
|
-
wip_limit: 1
|
|
47
|
-
code_paths:
|
|
48
|
-
- 'packages/@lumenflow/cli/**'
|
|
49
|
-
git:
|
|
50
|
-
requireRemote: false
|
|
51
|
-
experimental:
|
|
52
|
-
context_validation: false
|
|
53
|
-
`;
|
|
54
|
-
writeFileSync(join(baseDir, '.lumenflow.config.yaml'), configContent);
|
|
55
|
-
// Create minimal package.json
|
|
56
|
-
const packageJson = {
|
|
57
|
-
name: 'test-project',
|
|
58
|
-
version: '1.0.0',
|
|
59
|
-
type: 'module',
|
|
60
|
-
};
|
|
61
|
-
writeFileSync(join(baseDir, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
62
|
-
// Initialize git repo using execFileSync (safer than execSync)
|
|
63
|
-
execFileSync('git', ['init'], { cwd: baseDir, stdio: 'pipe' });
|
|
64
|
-
execFileSync('git', ['config', 'user.email', 'test@test.com'], { cwd: baseDir, stdio: 'pipe' });
|
|
65
|
-
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: baseDir, stdio: 'pipe' });
|
|
66
|
-
writeFileSync(join(baseDir, 'README.md'), '# Test Project\n');
|
|
67
|
-
execFileSync('git', ['add', '.'], { cwd: baseDir, stdio: 'pipe' });
|
|
68
|
-
execFileSync('git', ['commit', '-m', 'Initial commit'], { cwd: baseDir, stdio: 'pipe' });
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Helper to create a WU YAML file directly
|
|
72
|
-
*/
|
|
73
|
-
function createWUFile(baseDir, id, options = {}) {
|
|
74
|
-
const wuDir = join(baseDir, 'docs/04-operations/tasks/wu');
|
|
75
|
-
const wuPath = join(wuDir, `${id}.yaml`);
|
|
76
|
-
const doc = {
|
|
77
|
-
id,
|
|
78
|
-
title: options.title || TEST_TITLE,
|
|
79
|
-
lane: options.lane || TEST_LANE,
|
|
80
|
-
status: options.status || WU_STATUS.READY,
|
|
81
|
-
type: 'feature',
|
|
82
|
-
priority: 'P2',
|
|
83
|
-
created: '2026-02-03',
|
|
84
|
-
description: options.description || TEST_DESCRIPTION,
|
|
85
|
-
acceptance: options.acceptance || ['Test criterion 1', 'Test criterion 2'],
|
|
86
|
-
code_paths: options.codePaths || ['packages/@lumenflow/cli/src/__tests__'],
|
|
87
|
-
tests: {
|
|
88
|
-
unit: ['packages/@lumenflow/cli/src/__tests__/wu-lifecycle-integration.test.ts'],
|
|
89
|
-
},
|
|
90
|
-
exposure: 'backend-only',
|
|
91
|
-
};
|
|
92
|
-
writeFileSync(wuPath, stringifyYAML(doc));
|
|
93
|
-
return wuPath;
|
|
94
|
-
}
|
|
95
|
-
describe('WU Lifecycle Integration Tests (WU-1363)', () => {
|
|
96
|
-
let tempDir;
|
|
97
|
-
let originalCwd;
|
|
98
|
-
beforeEach(() => {
|
|
99
|
-
tempDir = join(tmpdir(), `wu-lifecycle-integration-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
100
|
-
mkdirSync(tempDir, { recursive: true });
|
|
101
|
-
originalCwd = process.cwd();
|
|
102
|
-
createTestProject(tempDir);
|
|
103
|
-
vi.resetModules(); // Reset module cache for fresh imports
|
|
104
|
-
});
|
|
105
|
-
afterEach(() => {
|
|
106
|
-
process.chdir(originalCwd);
|
|
107
|
-
if (existsSync(tempDir)) {
|
|
108
|
-
try {
|
|
109
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
// Ignore cleanup errors
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
vi.clearAllMocks();
|
|
116
|
-
});
|
|
117
|
-
describe('AC1: Integration tests for wu:create, wu:claim, wu:status', () => {
|
|
118
|
-
describe('wu:create core functionality', () => {
|
|
119
|
-
it('should create a WU YAML file with correct structure', async () => {
|
|
120
|
-
// Arrange
|
|
121
|
-
process.chdir(tempDir);
|
|
122
|
-
// Act - Create WU file directly (simulating wu:create core behavior)
|
|
123
|
-
const wuPath = createWUFile(tempDir, TEST_WU_ID, {
|
|
124
|
-
status: WU_STATUS.READY,
|
|
125
|
-
lane: TEST_LANE,
|
|
126
|
-
title: TEST_TITLE,
|
|
127
|
-
description: TEST_DESCRIPTION,
|
|
128
|
-
acceptance: ['Criterion 1', 'Criterion 2'],
|
|
129
|
-
codePaths: ['packages/@lumenflow/cli/src'],
|
|
130
|
-
});
|
|
131
|
-
// Assert
|
|
132
|
-
expect(existsSync(wuPath)).toBe(true);
|
|
133
|
-
const content = readFileSync(wuPath, 'utf-8');
|
|
134
|
-
const doc = parseYAML(content);
|
|
135
|
-
expect(doc.id).toBe(TEST_WU_ID);
|
|
136
|
-
expect(doc.lane).toBe(TEST_LANE);
|
|
137
|
-
expect(doc.status).toBe(WU_STATUS.READY);
|
|
138
|
-
expect(doc.title).toBe(TEST_TITLE);
|
|
139
|
-
expect(doc.acceptance).toHaveLength(2);
|
|
140
|
-
});
|
|
141
|
-
it('should validate required fields are present', () => {
|
|
142
|
-
// Arrange
|
|
143
|
-
process.chdir(tempDir);
|
|
144
|
-
// Create a minimal WU and verify required fields
|
|
145
|
-
const wuPath = createWUFile(tempDir, TEST_WU_ID);
|
|
146
|
-
const content = readFileSync(wuPath, 'utf-8');
|
|
147
|
-
const doc = parseYAML(content);
|
|
148
|
-
// Assert required fields exist
|
|
149
|
-
expect(doc.id).toBeDefined();
|
|
150
|
-
expect(doc.title).toBeDefined();
|
|
151
|
-
expect(doc.lane).toBeDefined();
|
|
152
|
-
expect(doc.status).toBeDefined();
|
|
153
|
-
expect(doc.description).toBeDefined();
|
|
154
|
-
expect(doc.acceptance).toBeDefined();
|
|
155
|
-
expect(doc.code_paths).toBeDefined();
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
describe('wu:claim core functionality', () => {
|
|
159
|
-
it('should update WU status to in_progress when claimed', async () => {
|
|
160
|
-
// Arrange
|
|
161
|
-
process.chdir(tempDir);
|
|
162
|
-
const wuPath = createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
|
|
163
|
-
// Act - Simulate claim by updating status
|
|
164
|
-
const content = readFileSync(wuPath, 'utf-8');
|
|
165
|
-
const doc = parseYAML(content);
|
|
166
|
-
doc.status = WU_STATUS.IN_PROGRESS;
|
|
167
|
-
doc.claimed_at = new Date().toISOString();
|
|
168
|
-
doc.worktree_path = `worktrees/framework-cli-${TEST_WU_ID.toLowerCase()}`;
|
|
169
|
-
writeFileSync(wuPath, stringifyYAML(doc));
|
|
170
|
-
// Assert
|
|
171
|
-
const updatedContent = readFileSync(wuPath, 'utf-8');
|
|
172
|
-
const updatedDoc = parseYAML(updatedContent);
|
|
173
|
-
expect(updatedDoc.status).toBe(WU_STATUS.IN_PROGRESS);
|
|
174
|
-
expect(updatedDoc.claimed_at).toBeDefined();
|
|
175
|
-
expect(updatedDoc.worktree_path).toContain('worktrees');
|
|
176
|
-
});
|
|
177
|
-
it('should reject claim when WU does not exist', () => {
|
|
178
|
-
// Arrange
|
|
179
|
-
process.chdir(tempDir);
|
|
180
|
-
const nonExistentPath = join(tempDir, 'docs/04-operations/tasks/wu', 'WU-9999.yaml');
|
|
181
|
-
// Assert
|
|
182
|
-
expect(existsSync(nonExistentPath)).toBe(false);
|
|
183
|
-
});
|
|
184
|
-
it('should reject claim when WU is not in ready status', () => {
|
|
185
|
-
// Arrange
|
|
186
|
-
process.chdir(tempDir);
|
|
187
|
-
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.DONE });
|
|
188
|
-
// Read and verify status prevents claiming
|
|
189
|
-
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
190
|
-
const content = readFileSync(wuPath, 'utf-8');
|
|
191
|
-
const doc = parseYAML(content);
|
|
192
|
-
// Assert
|
|
193
|
-
expect(doc.status).toBe(WU_STATUS.DONE);
|
|
194
|
-
expect(doc.status).not.toBe(WU_STATUS.READY);
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
describe('wu:status core functionality', () => {
|
|
198
|
-
it('should return WU details correctly', () => {
|
|
199
|
-
// Arrange
|
|
200
|
-
process.chdir(tempDir);
|
|
201
|
-
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
|
|
202
|
-
// Act - Read WU status
|
|
203
|
-
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
204
|
-
const content = readFileSync(wuPath, 'utf-8');
|
|
205
|
-
const doc = parseYAML(content);
|
|
206
|
-
// Assert
|
|
207
|
-
expect(doc.id).toBe(TEST_WU_ID);
|
|
208
|
-
expect(doc.status).toBe(WU_STATUS.READY);
|
|
209
|
-
expect(doc.lane).toBe(TEST_LANE);
|
|
210
|
-
});
|
|
211
|
-
it('should return valid commands for ready status', () => {
|
|
212
|
-
// Arrange
|
|
213
|
-
process.chdir(tempDir);
|
|
214
|
-
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
|
|
215
|
-
// Act
|
|
216
|
-
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
217
|
-
const content = readFileSync(wuPath, 'utf-8');
|
|
218
|
-
const doc = parseYAML(content);
|
|
219
|
-
// Compute valid commands based on status
|
|
220
|
-
const validCommands = doc.status === WU_STATUS.READY
|
|
221
|
-
? ['wu:claim', 'wu:edit', 'wu:delete']
|
|
222
|
-
: ['wu:prep', 'wu:block'];
|
|
223
|
-
// Assert
|
|
224
|
-
expect(validCommands).toContain('wu:claim');
|
|
225
|
-
});
|
|
226
|
-
it('should return valid commands for in_progress status', () => {
|
|
227
|
-
// Arrange
|
|
228
|
-
process.chdir(tempDir);
|
|
229
|
-
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
|
|
230
|
-
// Act
|
|
231
|
-
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
232
|
-
const content = readFileSync(wuPath, 'utf-8');
|
|
233
|
-
const doc = parseYAML(content);
|
|
234
|
-
// Compute valid commands based on status
|
|
235
|
-
const validCommands = doc.status === WU_STATUS.IN_PROGRESS ? ['wu:prep', 'wu:block'] : ['wu:claim'];
|
|
236
|
-
// Assert
|
|
237
|
-
expect(validCommands).toContain('wu:prep');
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
describe('AC2: Integration tests for wu:prep, wu:done workflow', () => {
|
|
242
|
-
describe('wu:prep core functionality', () => {
|
|
243
|
-
it('should validate WU is in in_progress status', () => {
|
|
244
|
-
// Arrange
|
|
245
|
-
process.chdir(tempDir);
|
|
246
|
-
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
|
|
247
|
-
// Act
|
|
248
|
-
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
249
|
-
const content = readFileSync(wuPath, 'utf-8');
|
|
250
|
-
const doc = parseYAML(content);
|
|
251
|
-
// Assert
|
|
252
|
-
expect(doc.status).toBe(WU_STATUS.IN_PROGRESS);
|
|
253
|
-
});
|
|
254
|
-
it('should generate next command pointing to wu:done', () => {
|
|
255
|
-
// Arrange
|
|
256
|
-
process.chdir(tempDir);
|
|
257
|
-
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
|
|
258
|
-
// Act - Generate next command
|
|
259
|
-
const mainCheckoutPath = tempDir;
|
|
260
|
-
const nextCommand = `cd ${mainCheckoutPath} && pnpm wu:done --id ${TEST_WU_ID}`;
|
|
261
|
-
// Assert
|
|
262
|
-
expect(nextCommand).toContain('wu:done');
|
|
263
|
-
expect(nextCommand).toContain(TEST_WU_ID);
|
|
264
|
-
expect(nextCommand).toContain(mainCheckoutPath);
|
|
265
|
-
});
|
|
266
|
-
it('should reject prep when WU is not in_progress', () => {
|
|
267
|
-
// Arrange
|
|
268
|
-
process.chdir(tempDir);
|
|
269
|
-
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
|
|
270
|
-
// Act
|
|
271
|
-
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
272
|
-
const content = readFileSync(wuPath, 'utf-8');
|
|
273
|
-
const doc = parseYAML(content);
|
|
274
|
-
// Assert
|
|
275
|
-
expect(doc.status).not.toBe(WU_STATUS.IN_PROGRESS);
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
describe('wu:done core functionality', () => {
|
|
279
|
-
it('should create stamp file on completion', () => {
|
|
280
|
-
// Arrange
|
|
281
|
-
process.chdir(tempDir);
|
|
282
|
-
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
|
|
283
|
-
// Act - Create stamp file
|
|
284
|
-
const stampDir = join(tempDir, '.lumenflow/stamps');
|
|
285
|
-
mkdirSync(stampDir, { recursive: true });
|
|
286
|
-
const stampPath = join(stampDir, `${TEST_WU_ID}.done`);
|
|
287
|
-
const stampContent = {
|
|
288
|
-
completed_at: new Date().toISOString(),
|
|
289
|
-
wu_id: TEST_WU_ID,
|
|
290
|
-
};
|
|
291
|
-
writeFileSync(stampPath, JSON.stringify(stampContent, null, 2));
|
|
292
|
-
// Assert
|
|
293
|
-
expect(existsSync(stampPath)).toBe(true);
|
|
294
|
-
const savedStamp = JSON.parse(readFileSync(stampPath, 'utf-8'));
|
|
295
|
-
expect(savedStamp.wu_id).toBe(TEST_WU_ID);
|
|
296
|
-
});
|
|
297
|
-
it('should update WU status to done', () => {
|
|
298
|
-
// Arrange
|
|
299
|
-
process.chdir(tempDir);
|
|
300
|
-
const wuPath = createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
|
|
301
|
-
// Act - Update status to done
|
|
302
|
-
const content = readFileSync(wuPath, 'utf-8');
|
|
303
|
-
const doc = parseYAML(content);
|
|
304
|
-
doc.status = WU_STATUS.DONE;
|
|
305
|
-
doc.completed_at = new Date().toISOString();
|
|
306
|
-
writeFileSync(wuPath, stringifyYAML(doc));
|
|
307
|
-
// Assert
|
|
308
|
-
const updatedContent = readFileSync(wuPath, 'utf-8');
|
|
309
|
-
const updatedDoc = parseYAML(updatedContent);
|
|
310
|
-
expect(updatedDoc.status).toBe(WU_STATUS.DONE);
|
|
311
|
-
expect(updatedDoc.completed_at).toBeDefined();
|
|
312
|
-
});
|
|
313
|
-
it('should reject done when WU is not in_progress', () => {
|
|
314
|
-
// Arrange
|
|
315
|
-
process.chdir(tempDir);
|
|
316
|
-
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
|
|
317
|
-
// Act
|
|
318
|
-
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
319
|
-
const content = readFileSync(wuPath, 'utf-8');
|
|
320
|
-
const doc = parseYAML(content);
|
|
321
|
-
// Assert - Cannot complete a WU that isn't in_progress
|
|
322
|
-
expect(doc.status).not.toBe(WU_STATUS.IN_PROGRESS);
|
|
323
|
-
});
|
|
324
|
-
it('should support skip-gates flag with reason and fix-wu', () => {
|
|
325
|
-
// Arrange
|
|
326
|
-
process.chdir(tempDir);
|
|
327
|
-
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
|
|
328
|
-
// Act - Simulate skip-gates audit log
|
|
329
|
-
const skipGatesLog = {
|
|
330
|
-
wu_id: TEST_WU_ID,
|
|
331
|
-
skipped_at: new Date().toISOString(),
|
|
332
|
-
reason: 'pre-existing on main',
|
|
333
|
-
fix_wu: 'WU-1234',
|
|
334
|
-
};
|
|
335
|
-
const auditPath = join(tempDir, '.lumenflow/skip-gates-audit.log');
|
|
336
|
-
writeFileSync(auditPath, JSON.stringify(skipGatesLog) + '\n');
|
|
337
|
-
// Assert
|
|
338
|
-
expect(existsSync(auditPath)).toBe(true);
|
|
339
|
-
const logContent = readFileSync(auditPath, 'utf-8');
|
|
340
|
-
expect(logContent).toContain('pre-existing on main');
|
|
341
|
-
expect(logContent).toContain('WU-1234');
|
|
342
|
-
});
|
|
343
|
-
});
|
|
344
|
-
describe('wu:prep + wu:done complete workflow', () => {
|
|
345
|
-
it('should complete full lifecycle from create to done', () => {
|
|
346
|
-
// This test validates the complete workflow state transitions:
|
|
347
|
-
// 1. Create WU (status: ready)
|
|
348
|
-
// 2. Claim WU (status: in_progress, worktree created)
|
|
349
|
-
// 3. Prep WU (gates run, provides next command)
|
|
350
|
-
// 4. Done WU (status: done, stamp created)
|
|
351
|
-
// Arrange
|
|
352
|
-
process.chdir(tempDir);
|
|
353
|
-
// Step 1: Create WU
|
|
354
|
-
const wuPath = createWUFile(tempDir, TEST_WU_ID, {
|
|
355
|
-
status: WU_STATUS.READY,
|
|
356
|
-
lane: TEST_LANE,
|
|
357
|
-
title: TEST_TITLE,
|
|
358
|
-
description: TEST_DESCRIPTION,
|
|
359
|
-
acceptance: ['Full lifecycle test'],
|
|
360
|
-
});
|
|
361
|
-
expect(existsSync(wuPath)).toBe(true);
|
|
362
|
-
// Step 2: Simulate Claim WU
|
|
363
|
-
let doc = parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
364
|
-
expect(doc.status).toBe(WU_STATUS.READY);
|
|
365
|
-
doc.status = WU_STATUS.IN_PROGRESS;
|
|
366
|
-
doc.claimed_at = new Date().toISOString();
|
|
367
|
-
doc.worktree_path = `worktrees/framework-cli-${TEST_WU_ID.toLowerCase()}`;
|
|
368
|
-
writeFileSync(wuPath, stringifyYAML(doc));
|
|
369
|
-
// Step 3: Verify Prep is valid (status check)
|
|
370
|
-
doc = parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
371
|
-
expect(doc.status).toBe(WU_STATUS.IN_PROGRESS);
|
|
372
|
-
const nextCommand = `cd ${tempDir} && pnpm wu:done --id ${TEST_WU_ID}`;
|
|
373
|
-
expect(nextCommand).toContain('wu:done');
|
|
374
|
-
// Step 4: Complete WU
|
|
375
|
-
doc.status = WU_STATUS.DONE;
|
|
376
|
-
doc.completed_at = new Date().toISOString();
|
|
377
|
-
writeFileSync(wuPath, stringifyYAML(doc));
|
|
378
|
-
// Create stamp
|
|
379
|
-
const stampPath = join(tempDir, '.lumenflow/stamps', `${TEST_WU_ID}.done`);
|
|
380
|
-
writeFileSync(stampPath, JSON.stringify({ completed_at: new Date().toISOString() }));
|
|
381
|
-
// Verify final state
|
|
382
|
-
expect(existsSync(stampPath)).toBe(true);
|
|
383
|
-
doc = parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
384
|
-
expect(doc.status).toBe(WU_STATUS.DONE);
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
vi.mock('node:child_process', async (importOriginal) => {
|
|
3
|
-
const actual = await importOriginal();
|
|
4
|
-
return { ...actual, spawnSync: vi.fn() };
|
|
5
|
-
});
|
|
6
|
-
vi.mock('node:fs', async (importOriginal) => {
|
|
7
|
-
const actual = await importOriginal();
|
|
8
|
-
return { ...actual, existsSync: vi.fn() };
|
|
9
|
-
});
|
|
10
|
-
describe('wu-prep default exec helpers (WU-1441)', () => {
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
vi.resetModules();
|
|
13
|
-
vi.resetAllMocks();
|
|
14
|
-
});
|
|
15
|
-
it('uses node + dist wu-validate for JSON comparison when available', async () => {
|
|
16
|
-
const { spawnSync } = await import('node:child_process');
|
|
17
|
-
const { existsSync } = await import('node:fs');
|
|
18
|
-
// Pretend dist sibling exists so defaultExec picks node+dist path (not pnpm on main).
|
|
19
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
20
|
-
// Both worktree and main should report WU-1 invalid.
|
|
21
|
-
vi.mocked(spawnSync).mockReturnValue({
|
|
22
|
-
status: 1,
|
|
23
|
-
stdout: JSON.stringify({ invalid: [{ wuId: 'WU-1' }] }),
|
|
24
|
-
stderr: '',
|
|
25
|
-
});
|
|
26
|
-
const { checkPreExistingFailures } = await import('../wu-prep.js');
|
|
27
|
-
const result = await checkPreExistingFailures({ mainCheckout: '/repo' });
|
|
28
|
-
expect(result.error).toBeUndefined();
|
|
29
|
-
expect(result.hasPreExisting).toBe(true);
|
|
30
|
-
expect(result.hasNewFailures).toBe(false);
|
|
31
|
-
// Default exec should run node directly, not "pnpm wu:validate" from the main checkout.
|
|
32
|
-
expect(vi.mocked(spawnSync).mock.calls.length).toBeGreaterThan(0);
|
|
33
|
-
expect(vi.mocked(spawnSync).mock.calls[0]?.[0]).toBe('node');
|
|
34
|
-
});
|
|
35
|
-
});
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import * as locationResolver from '@lumenflow/core/dist/context/location-resolver.js';
|
|
3
|
-
import * as wuYaml from '@lumenflow/core/dist/wu-yaml.js';
|
|
4
|
-
import { CONTEXT_VALIDATION, WU_STATUS } from '@lumenflow/core/dist/wu-constants.js';
|
|
5
|
-
const { LOCATION_TYPES } = CONTEXT_VALIDATION;
|
|
6
|
-
// Mock dependencies
|
|
7
|
-
vi.mock('@lumenflow/core/dist/context/location-resolver.js');
|
|
8
|
-
vi.mock('@lumenflow/core/dist/error-handler.js');
|
|
9
|
-
vi.mock('@lumenflow/core/dist/wu-yaml.js');
|
|
10
|
-
vi.mock('../gates.js', () => ({
|
|
11
|
-
runGates: vi.fn().mockResolvedValue(true),
|
|
12
|
-
}));
|
|
13
|
-
describe('wu-prep (WU-1223)', () => {
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
vi.resetAllMocks();
|
|
16
|
-
});
|
|
17
|
-
describe('location validation', () => {
|
|
18
|
-
it('should error when run from main checkout', async () => {
|
|
19
|
-
// Mock location as main checkout
|
|
20
|
-
vi.mocked(locationResolver.resolveLocation).mockResolvedValue({
|
|
21
|
-
type: LOCATION_TYPES.MAIN,
|
|
22
|
-
cwd: '/repo',
|
|
23
|
-
gitRoot: '/repo',
|
|
24
|
-
mainCheckout: '/repo',
|
|
25
|
-
worktreeName: null,
|
|
26
|
-
worktreeWuId: null,
|
|
27
|
-
});
|
|
28
|
-
// Import after mocks are set up
|
|
29
|
-
const { resolveLocation } = await import('@lumenflow/core/dist/context/location-resolver.js');
|
|
30
|
-
const location = await resolveLocation();
|
|
31
|
-
// Verify the mock returns main
|
|
32
|
-
expect(location.type).toBe(LOCATION_TYPES.MAIN);
|
|
33
|
-
});
|
|
34
|
-
it('should proceed when run from worktree', async () => {
|
|
35
|
-
// Mock location as worktree
|
|
36
|
-
vi.mocked(locationResolver.resolveLocation).mockResolvedValue({
|
|
37
|
-
type: LOCATION_TYPES.WORKTREE,
|
|
38
|
-
cwd: '/repo/worktrees/framework-cli-wu-1223',
|
|
39
|
-
gitRoot: '/repo/worktrees/framework-cli-wu-1223',
|
|
40
|
-
mainCheckout: '/repo',
|
|
41
|
-
worktreeName: 'framework-cli-wu-1223',
|
|
42
|
-
worktreeWuId: 'WU-1223',
|
|
43
|
-
});
|
|
44
|
-
const { resolveLocation } = await import('@lumenflow/core/dist/context/location-resolver.js');
|
|
45
|
-
const location = await resolveLocation();
|
|
46
|
-
// Verify the mock returns worktree
|
|
47
|
-
expect(location.type).toBe(LOCATION_TYPES.WORKTREE);
|
|
48
|
-
expect(location.mainCheckout).toBe('/repo');
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
describe('WU status validation', () => {
|
|
52
|
-
it('should only allow in_progress WUs', async () => {
|
|
53
|
-
// Mock WU YAML with wrong status
|
|
54
|
-
const mockDoc = {
|
|
55
|
-
id: 'WU-1223',
|
|
56
|
-
status: WU_STATUS.DONE,
|
|
57
|
-
title: 'Test WU',
|
|
58
|
-
};
|
|
59
|
-
vi.mocked(wuYaml.readWU).mockReturnValue(mockDoc);
|
|
60
|
-
const { readWU } = await import('@lumenflow/core/dist/wu-yaml.js');
|
|
61
|
-
const doc = readWU('path/to/wu.yaml', 'WU-1223');
|
|
62
|
-
expect(doc.status).toBe(WU_STATUS.DONE);
|
|
63
|
-
expect(doc.status).not.toBe(WU_STATUS.IN_PROGRESS);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
describe('success message', () => {
|
|
67
|
-
it('should include copy-paste instruction with main path', async () => {
|
|
68
|
-
// The success message should include:
|
|
69
|
-
// 1. Main checkout path
|
|
70
|
-
// 2. WU ID
|
|
71
|
-
// 3. Copy-paste command: cd <main> && pnpm wu:done --id <WU-ID>
|
|
72
|
-
const mainCheckout = '/repo';
|
|
73
|
-
const wuId = 'WU-1223';
|
|
74
|
-
// Build expected command that would be in the success message
|
|
75
|
-
const expectedCommand = `cd ${mainCheckout} && pnpm wu:done --id ${wuId}`;
|
|
76
|
-
expect(expectedCommand).toBe('cd /repo && pnpm wu:done --id WU-1223');
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
describe('wu:done worktree check (WU-1223)', () => {
|
|
81
|
-
beforeEach(() => {
|
|
82
|
-
vi.resetAllMocks();
|
|
83
|
-
});
|
|
84
|
-
it('should error when run from worktree with guidance to use wu:prep', async () => {
|
|
85
|
-
// Mock location as worktree
|
|
86
|
-
vi.mocked(locationResolver.resolveLocation).mockResolvedValue({
|
|
87
|
-
type: LOCATION_TYPES.WORKTREE,
|
|
88
|
-
cwd: '/repo/worktrees/framework-cli-wu-1223',
|
|
89
|
-
gitRoot: '/repo/worktrees/framework-cli-wu-1223',
|
|
90
|
-
mainCheckout: '/repo',
|
|
91
|
-
worktreeName: 'framework-cli-wu-1223',
|
|
92
|
-
worktreeWuId: 'WU-1223',
|
|
93
|
-
});
|
|
94
|
-
const { resolveLocation } = await import('@lumenflow/core/dist/context/location-resolver.js');
|
|
95
|
-
const location = await resolveLocation();
|
|
96
|
-
// The error message should guide user to wu:prep workflow
|
|
97
|
-
expect(location.type).toBe(LOCATION_TYPES.WORKTREE);
|
|
98
|
-
// Error message should contain:
|
|
99
|
-
const errorShouldContain = [
|
|
100
|
-
'wu:prep', // Mention the new command
|
|
101
|
-
'main checkout', // Explain where wu:done should run
|
|
102
|
-
'/repo', // Main checkout path
|
|
103
|
-
];
|
|
104
|
-
// Build the expected error content
|
|
105
|
-
const expectedGuidance = `pnpm wu:prep --id WU-1223`;
|
|
106
|
-
expect(expectedGuidance).toContain('wu:prep');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
describe('wu-prep spec-linter classification (WU-1441)', () => {
|
|
110
|
-
it('should detect pre-existing failures only', async () => {
|
|
111
|
-
const { classifySpecLinterFailures } = await import('../wu-prep.js');
|
|
112
|
-
const result = classifySpecLinterFailures({
|
|
113
|
-
mainInvalid: ['WU-1'],
|
|
114
|
-
worktreeInvalid: ['WU-1'],
|
|
115
|
-
});
|
|
116
|
-
expect(result.hasPreExisting).toBe(true);
|
|
117
|
-
expect(result.hasNewFailures).toBe(false);
|
|
118
|
-
expect(result.newFailures).toEqual([]);
|
|
119
|
-
});
|
|
120
|
-
it('should detect newly introduced failures', async () => {
|
|
121
|
-
const { classifySpecLinterFailures } = await import('../wu-prep.js');
|
|
122
|
-
const result = classifySpecLinterFailures({
|
|
123
|
-
mainInvalid: ['WU-1'],
|
|
124
|
-
worktreeInvalid: ['WU-1', 'WU-2'],
|
|
125
|
-
});
|
|
126
|
-
expect(result.hasPreExisting).toBe(true);
|
|
127
|
-
expect(result.hasNewFailures).toBe(true);
|
|
128
|
-
expect(result.newFailures).toEqual(['WU-2']);
|
|
129
|
-
});
|
|
130
|
-
it('should detect failures when main is clean', async () => {
|
|
131
|
-
const { classifySpecLinterFailures } = await import('../wu-prep.js');
|
|
132
|
-
const result = classifySpecLinterFailures({
|
|
133
|
-
mainInvalid: [],
|
|
134
|
-
worktreeInvalid: ['WU-3'],
|
|
135
|
-
});
|
|
136
|
-
expect(result.hasPreExisting).toBe(false);
|
|
137
|
-
expect(result.hasNewFailures).toBe(true);
|
|
138
|
-
expect(result.newFailures).toEqual(['WU-3']);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file wu-proto.test.ts
|
|
3
|
-
* Test suite for wu:proto command (WU-1359)
|
|
4
|
-
*
|
|
5
|
-
* WU-1359: Add wu:proto convenience command for rapid prototyping
|
|
6
|
-
*
|
|
7
|
-
* Tests:
|
|
8
|
-
* - wu:proto creates WU with type: prototype
|
|
9
|
-
* - wu:proto has relaxed validation (no --acceptance required)
|
|
10
|
-
* - wu:proto immediately claims the WU
|
|
11
|
-
* - wu:proto prints cd command to worktree
|
|
12
|
-
*/
|
|
13
|
-
import { describe, it, expect } from 'vitest';
|
|
14
|
-
// WU-1359: Import wu:proto validation and helpers
|
|
15
|
-
import { validateProtoSpec } from '../wu-proto.js';
|
|
16
|
-
/** Test constants to avoid duplicate string literals (sonarjs/no-duplicate-string) */
|
|
17
|
-
const TEST_WU_ID = 'WU-9999';
|
|
18
|
-
const TEST_LANE = 'Framework: CLI';
|
|
19
|
-
const TEST_TITLE = 'Quick prototype';
|
|
20
|
-
const TEST_DESCRIPTION = 'Context: testing.\nProblem: need to test.\nSolution: add tests.';
|
|
21
|
-
describe('wu:proto command (WU-1359)', () => {
|
|
22
|
-
describe('validateProtoSpec relaxed validation', () => {
|
|
23
|
-
it('should pass validation without --acceptance', () => {
|
|
24
|
-
const result = validateProtoSpec({
|
|
25
|
-
id: TEST_WU_ID,
|
|
26
|
-
lane: TEST_LANE,
|
|
27
|
-
title: TEST_TITLE,
|
|
28
|
-
opts: {
|
|
29
|
-
description: TEST_DESCRIPTION,
|
|
30
|
-
codePaths: ['packages/@lumenflow/cli/src/wu-proto.ts'],
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
expect(result.valid).toBe(true);
|
|
34
|
-
expect(result.errors).toHaveLength(0);
|
|
35
|
-
});
|
|
36
|
-
it('should pass validation without --exposure', () => {
|
|
37
|
-
const result = validateProtoSpec({
|
|
38
|
-
id: TEST_WU_ID,
|
|
39
|
-
lane: TEST_LANE,
|
|
40
|
-
title: TEST_TITLE,
|
|
41
|
-
opts: {
|
|
42
|
-
description: TEST_DESCRIPTION,
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
expect(result.valid).toBe(true);
|
|
46
|
-
expect(result.errors).toHaveLength(0);
|
|
47
|
-
});
|
|
48
|
-
it('should pass validation without --code-paths', () => {
|
|
49
|
-
const result = validateProtoSpec({
|
|
50
|
-
id: TEST_WU_ID,
|
|
51
|
-
lane: TEST_LANE,
|
|
52
|
-
title: TEST_TITLE,
|
|
53
|
-
opts: {
|
|
54
|
-
description: TEST_DESCRIPTION,
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
expect(result.valid).toBe(true);
|
|
58
|
-
expect(result.errors).toHaveLength(0);
|
|
59
|
-
});
|
|
60
|
-
it('should require lane', () => {
|
|
61
|
-
const result = validateProtoSpec({
|
|
62
|
-
id: TEST_WU_ID,
|
|
63
|
-
lane: '',
|
|
64
|
-
title: TEST_TITLE,
|
|
65
|
-
opts: {},
|
|
66
|
-
});
|
|
67
|
-
expect(result.valid).toBe(false);
|
|
68
|
-
expect(result.errors.some((e) => e.toLowerCase().includes('lane'))).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
it('should require title', () => {
|
|
71
|
-
const result = validateProtoSpec({
|
|
72
|
-
id: TEST_WU_ID,
|
|
73
|
-
lane: TEST_LANE,
|
|
74
|
-
title: '',
|
|
75
|
-
opts: {},
|
|
76
|
-
});
|
|
77
|
-
expect(result.valid).toBe(false);
|
|
78
|
-
expect(result.errors.some((e) => e.toLowerCase().includes('title'))).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
describe('prototype WU type', () => {
|
|
82
|
-
it('should set type to prototype', () => {
|
|
83
|
-
// The buildProtoWUContent function should return type: prototype
|
|
84
|
-
// We test this indirectly through the validateProtoSpec which checks content
|
|
85
|
-
const result = validateProtoSpec({
|
|
86
|
-
id: TEST_WU_ID,
|
|
87
|
-
lane: TEST_LANE,
|
|
88
|
-
title: TEST_TITLE,
|
|
89
|
-
opts: {
|
|
90
|
-
description: TEST_DESCRIPTION,
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
// Prototype WUs should always be valid with minimal input
|
|
94
|
-
expect(result.valid).toBe(true);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
});
|