@lumenflow/cli 2.4.0 → 2.5.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/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 +96 -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__/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 +580 -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/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/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-spawn.js +8 -0
- package/dist/wu-unblock.js +34 -2
- package/dist/wu-validate.js +25 -17
- package/package.json +10 -6
- package/templates/core/AGENTS.md.template +2 -2
- 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,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file init-config-lanes.test.ts
|
|
3
|
+
* Test: .lumenflow.config.yaml includes default lane definitions for parent lanes
|
|
4
|
+
*
|
|
5
|
+
* WU-1307: Fix lumenflow-init scaffolding
|
|
6
|
+
*
|
|
7
|
+
* The generated config must include lane definitions that match the parent
|
|
8
|
+
* lanes used in the documentation examples (Framework, Experience, Content, Operations).
|
|
9
|
+
* These lanes should have sensible defaults for code_paths and wip_limit.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import * as os from 'node:os';
|
|
15
|
+
import YAML from 'yaml';
|
|
16
|
+
import { scaffoldProject } from '../init.js';
|
|
17
|
+
/** Config file name - extracted to avoid duplicate string lint errors */
|
|
18
|
+
const CONFIG_FILE_NAME = '.lumenflow.config.yaml';
|
|
19
|
+
describe('init config default lanes (WU-1307)', () => {
|
|
20
|
+
let tempDir;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
// Create a temporary directory for each test
|
|
23
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-init-config-lanes-'));
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
// Clean up temporary directory
|
|
27
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
28
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
/** Helper to read and parse config from temp directory */
|
|
32
|
+
function readConfig() {
|
|
33
|
+
const configPath = path.join(tempDir, CONFIG_FILE_NAME);
|
|
34
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
35
|
+
return YAML.parse(configContent);
|
|
36
|
+
}
|
|
37
|
+
it('should generate .lumenflow.config.yaml with lanes.definitions', async () => {
|
|
38
|
+
// Arrange
|
|
39
|
+
const configPath = path.join(tempDir, CONFIG_FILE_NAME);
|
|
40
|
+
// Act
|
|
41
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
42
|
+
// Assert
|
|
43
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
44
|
+
const config = readConfig();
|
|
45
|
+
// Should have lanes.definitions
|
|
46
|
+
expect(config.lanes).toBeDefined();
|
|
47
|
+
expect(config.lanes.definitions).toBeDefined();
|
|
48
|
+
expect(Array.isArray(config.lanes.definitions)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it('should include Framework parent lane with sublanes', async () => {
|
|
51
|
+
// Act
|
|
52
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
53
|
+
// Assert
|
|
54
|
+
const config = readConfig();
|
|
55
|
+
const lanes = (config.lanes?.definitions || []);
|
|
56
|
+
const frameworkLanes = lanes.filter((l) => l.name.startsWith('Framework:'));
|
|
57
|
+
expect(frameworkLanes.length).toBeGreaterThan(0);
|
|
58
|
+
// Should have at least Framework: Core and Framework: CLI
|
|
59
|
+
const laneNames = frameworkLanes.map((l) => l.name);
|
|
60
|
+
expect(laneNames).toContain('Framework: Core');
|
|
61
|
+
expect(laneNames).toContain('Framework: CLI');
|
|
62
|
+
});
|
|
63
|
+
it('should include Experience parent lane for frontend work', async () => {
|
|
64
|
+
// Act
|
|
65
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
66
|
+
// Assert
|
|
67
|
+
const config = readConfig();
|
|
68
|
+
const lanes = (config.lanes?.definitions || []);
|
|
69
|
+
const experienceLanes = lanes.filter((l) => l.name.startsWith('Experience:'));
|
|
70
|
+
// Should have at least one Experience lane
|
|
71
|
+
expect(experienceLanes.length).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
it('should include Content: Documentation lane', async () => {
|
|
74
|
+
// Act
|
|
75
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
76
|
+
// Assert
|
|
77
|
+
const config = readConfig();
|
|
78
|
+
const lanes = (config.lanes?.definitions || []);
|
|
79
|
+
const contentLane = lanes.find((l) => l.name === 'Content: Documentation');
|
|
80
|
+
expect(contentLane).toBeDefined();
|
|
81
|
+
expect(contentLane?.code_paths).toBeDefined();
|
|
82
|
+
expect(contentLane?.code_paths).toContain('docs/**');
|
|
83
|
+
});
|
|
84
|
+
it('should include Operations parent lanes', async () => {
|
|
85
|
+
// Act
|
|
86
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
87
|
+
// Assert
|
|
88
|
+
const config = readConfig();
|
|
89
|
+
const lanes = (config.lanes?.definitions || []);
|
|
90
|
+
const operationsLanes = lanes.filter((l) => l.name.startsWith('Operations:'));
|
|
91
|
+
expect(operationsLanes.length).toBeGreaterThan(0);
|
|
92
|
+
// Should have Infrastructure and CI/CD
|
|
93
|
+
const laneNames = operationsLanes.map((l) => l.name);
|
|
94
|
+
expect(laneNames).toContain('Operations: Infrastructure');
|
|
95
|
+
expect(laneNames).toContain('Operations: CI/CD');
|
|
96
|
+
});
|
|
97
|
+
it('should have wip_limit: 1 for code lanes by default', async () => {
|
|
98
|
+
// Act
|
|
99
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
100
|
+
// Assert
|
|
101
|
+
const config = readConfig();
|
|
102
|
+
const lanes = (config.lanes?.definitions || []);
|
|
103
|
+
const frameworkCore = lanes.find((l) => l.name === 'Framework: Core');
|
|
104
|
+
expect(frameworkCore).toBeDefined();
|
|
105
|
+
expect(frameworkCore?.wip_limit).toBe(1);
|
|
106
|
+
});
|
|
107
|
+
it('should have code_paths for each lane', async () => {
|
|
108
|
+
// Act
|
|
109
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
110
|
+
// Assert
|
|
111
|
+
const config = readConfig();
|
|
112
|
+
const lanes = (config.lanes?.definitions || []);
|
|
113
|
+
// Every lane should have code_paths
|
|
114
|
+
for (const lane of lanes) {
|
|
115
|
+
expect(lane.code_paths).toBeDefined();
|
|
116
|
+
expect(Array.isArray(lane.code_paths)).toBe(true);
|
|
117
|
+
expect(lane.code_paths?.length).toBeGreaterThan(0);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
it('should use "Parent: Sublane" format for lane names', async () => {
|
|
121
|
+
// Act
|
|
122
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
123
|
+
// Assert
|
|
124
|
+
const config = readConfig();
|
|
125
|
+
const lanes = (config.lanes?.definitions || []);
|
|
126
|
+
// All lanes should follow "Parent: Sublane" format (colon + space)
|
|
127
|
+
for (const lane of lanes) {
|
|
128
|
+
expect(lane.name).toMatch(/^[A-Z][a-z]+: [A-Z]/);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file init-docs-structure.test.ts
|
|
3
|
+
* Tests for --docs-structure flag and auto-detection (WU-1309)
|
|
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 { scaffoldProject, detectDocsStructure, getDocsPath, } from '../init.js';
|
|
10
|
+
// Constants to avoid duplicate strings (sonarjs/no-duplicate-string)
|
|
11
|
+
const ARC42_DOCS_STRUCTURE = 'arc42';
|
|
12
|
+
const SIMPLE_DOCS_STRUCTURE = 'simple';
|
|
13
|
+
const DOCS_04_OPERATIONS = '04-operations';
|
|
14
|
+
describe('docs-structure', () => {
|
|
15
|
+
let tempDir;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-docs-structure-test-'));
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
describe('detectDocsStructure', () => {
|
|
23
|
+
it('should return "arc42" when docs/04-operations exists', () => {
|
|
24
|
+
fs.mkdirSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS), { recursive: true });
|
|
25
|
+
const result = detectDocsStructure(tempDir);
|
|
26
|
+
expect(result).toBe(ARC42_DOCS_STRUCTURE);
|
|
27
|
+
});
|
|
28
|
+
it('should return "simple" when docs exists without 04-operations', () => {
|
|
29
|
+
fs.mkdirSync(path.join(tempDir, 'docs'), { recursive: true });
|
|
30
|
+
fs.writeFileSync(path.join(tempDir, 'docs', 'README.md'), '# Docs\n');
|
|
31
|
+
const result = detectDocsStructure(tempDir);
|
|
32
|
+
expect(result).toBe(SIMPLE_DOCS_STRUCTURE);
|
|
33
|
+
});
|
|
34
|
+
it('should return "simple" when no docs directory exists', () => {
|
|
35
|
+
const result = detectDocsStructure(tempDir);
|
|
36
|
+
expect(result).toBe(SIMPLE_DOCS_STRUCTURE);
|
|
37
|
+
});
|
|
38
|
+
it('should detect arc42 with any numbered directory (01-*, 02-*, etc.)', () => {
|
|
39
|
+
fs.mkdirSync(path.join(tempDir, 'docs', '01-introduction'), { recursive: true });
|
|
40
|
+
const result = detectDocsStructure(tempDir);
|
|
41
|
+
expect(result).toBe(ARC42_DOCS_STRUCTURE);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('getDocsPath', () => {
|
|
45
|
+
it('should return simple paths for simple structure', () => {
|
|
46
|
+
const paths = getDocsPath(SIMPLE_DOCS_STRUCTURE);
|
|
47
|
+
expect(paths.operations).toBe('docs');
|
|
48
|
+
expect(paths.tasks).toBe('docs/tasks');
|
|
49
|
+
expect(paths.onboarding).toBe('docs/_frameworks/lumenflow/agent/onboarding');
|
|
50
|
+
});
|
|
51
|
+
it('should return arc42 paths for arc42 structure', () => {
|
|
52
|
+
const paths = getDocsPath(ARC42_DOCS_STRUCTURE);
|
|
53
|
+
expect(paths.operations).toBe('docs/04-operations');
|
|
54
|
+
expect(paths.tasks).toBe('docs/04-operations/tasks');
|
|
55
|
+
expect(paths.onboarding).toBe('docs/04-operations/_frameworks/lumenflow/agent/onboarding');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('scaffoldProject with --docs-structure', () => {
|
|
59
|
+
it('should scaffold simple structure with --docs-structure simple', async () => {
|
|
60
|
+
const options = {
|
|
61
|
+
force: false,
|
|
62
|
+
full: true,
|
|
63
|
+
docsStructure: SIMPLE_DOCS_STRUCTURE,
|
|
64
|
+
};
|
|
65
|
+
await scaffoldProject(tempDir, options);
|
|
66
|
+
// Simple structure: docs/tasks, not docs/04-operations/tasks
|
|
67
|
+
expect(fs.existsSync(path.join(tempDir, 'docs', 'tasks'))).toBe(true);
|
|
68
|
+
expect(fs.existsSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS))).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
it('should scaffold arc42 structure with --docs-structure arc42', async () => {
|
|
71
|
+
const options = {
|
|
72
|
+
force: false,
|
|
73
|
+
full: true,
|
|
74
|
+
docsStructure: ARC42_DOCS_STRUCTURE,
|
|
75
|
+
};
|
|
76
|
+
await scaffoldProject(tempDir, options);
|
|
77
|
+
// Arc42 structure: docs/04-operations/tasks
|
|
78
|
+
expect(fs.existsSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS, 'tasks'))).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
it('should auto-detect arc42 when docs/04-operations exists', async () => {
|
|
81
|
+
// Create existing arc42 structure
|
|
82
|
+
fs.mkdirSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS), { recursive: true });
|
|
83
|
+
const options = {
|
|
84
|
+
force: false,
|
|
85
|
+
full: true,
|
|
86
|
+
// No docsStructure specified - should auto-detect arc42
|
|
87
|
+
};
|
|
88
|
+
await scaffoldProject(tempDir, options);
|
|
89
|
+
// Should use arc42 structure
|
|
90
|
+
expect(fs.existsSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS, 'tasks'))).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
it('should auto-detect simple when only docs exists', async () => {
|
|
93
|
+
// Create existing simple structure
|
|
94
|
+
fs.mkdirSync(path.join(tempDir, 'docs'), { recursive: true });
|
|
95
|
+
fs.writeFileSync(path.join(tempDir, 'docs', 'README.md'), '# Docs\n');
|
|
96
|
+
const options = {
|
|
97
|
+
force: false,
|
|
98
|
+
full: true,
|
|
99
|
+
// No docsStructure specified - should auto-detect simple
|
|
100
|
+
};
|
|
101
|
+
await scaffoldProject(tempDir, options);
|
|
102
|
+
// Should use simple structure
|
|
103
|
+
expect(fs.existsSync(path.join(tempDir, 'docs', 'tasks'))).toBe(true);
|
|
104
|
+
expect(fs.existsSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS))).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
it('should respect explicit --docs-structure over auto-detection', async () => {
|
|
107
|
+
// Create existing arc42 structure
|
|
108
|
+
fs.mkdirSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS), { recursive: true });
|
|
109
|
+
const options = {
|
|
110
|
+
force: true, // Force overwrite
|
|
111
|
+
full: true,
|
|
112
|
+
docsStructure: SIMPLE_DOCS_STRUCTURE, // Explicitly request simple
|
|
113
|
+
};
|
|
114
|
+
await scaffoldProject(tempDir, options);
|
|
115
|
+
// Should use simple structure despite arc42 existing
|
|
116
|
+
expect(fs.existsSync(path.join(tempDir, 'docs', 'tasks'))).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file init-lane-inference.test.ts
|
|
3
|
+
* Test: .lumenflow.lane-inference.yaml is generated in hierarchical Parent→Sublane format
|
|
4
|
+
*
|
|
5
|
+
* WU-1307: Fix lumenflow-init scaffolding
|
|
6
|
+
*
|
|
7
|
+
* The generated lane inference config must use the hierarchical format that
|
|
8
|
+
* lane-inference.ts/lane-checker.ts expect:
|
|
9
|
+
*
|
|
10
|
+
* Parent:
|
|
11
|
+
* Sublane:
|
|
12
|
+
* code_paths:
|
|
13
|
+
* - pattern
|
|
14
|
+
* keywords:
|
|
15
|
+
* - keyword
|
|
16
|
+
*
|
|
17
|
+
* NOT the flat format:
|
|
18
|
+
* lanes:
|
|
19
|
+
* - name: "Parent: Sublane"
|
|
20
|
+
* patterns:
|
|
21
|
+
* - pattern
|
|
22
|
+
*/
|
|
23
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
import * as os from 'node:os';
|
|
27
|
+
import YAML from 'yaml';
|
|
28
|
+
import { scaffoldProject } from '../init.js';
|
|
29
|
+
/** Lane inference file name - extracted to avoid duplicate string lint errors */
|
|
30
|
+
const LANE_INFERENCE_FILE_NAME = '.lumenflow.lane-inference.yaml';
|
|
31
|
+
describe('init lane inference generation (WU-1307)', () => {
|
|
32
|
+
let tempDir;
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
// Create a temporary directory for each test
|
|
35
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-init-lane-inference-'));
|
|
36
|
+
});
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
// Clean up temporary directory
|
|
39
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
40
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
/** Helper to read and parse lane inference config from temp directory */
|
|
44
|
+
function readLaneInference() {
|
|
45
|
+
const laneInferencePath = path.join(tempDir, LANE_INFERENCE_FILE_NAME);
|
|
46
|
+
const laneInferenceContent = fs.readFileSync(laneInferencePath, 'utf-8');
|
|
47
|
+
return YAML.parse(laneInferenceContent);
|
|
48
|
+
}
|
|
49
|
+
it('should generate .lumenflow.lane-inference.yaml in hierarchical format', async () => {
|
|
50
|
+
// Arrange
|
|
51
|
+
const laneInferencePath = path.join(tempDir, LANE_INFERENCE_FILE_NAME);
|
|
52
|
+
// Act
|
|
53
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
54
|
+
// Assert
|
|
55
|
+
expect(fs.existsSync(laneInferencePath)).toBe(true);
|
|
56
|
+
const laneInference = readLaneInference();
|
|
57
|
+
// Should NOT have a flat 'lanes' array
|
|
58
|
+
expect(laneInference.lanes).toBeUndefined();
|
|
59
|
+
// Should have hierarchical Parent -> Sublane structure
|
|
60
|
+
// At minimum, should have Framework, Operations, Content parents
|
|
61
|
+
expect(laneInference.Framework).toBeDefined();
|
|
62
|
+
expect(laneInference.Operations).toBeDefined();
|
|
63
|
+
expect(laneInference.Content).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
it('should include sublanes under parent lanes', async () => {
|
|
66
|
+
// Act
|
|
67
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
68
|
+
// Assert
|
|
69
|
+
const laneInference = readLaneInference();
|
|
70
|
+
// Framework parent should have sublanes like Core, CLI
|
|
71
|
+
expect(laneInference.Framework?.Core).toBeDefined();
|
|
72
|
+
expect(laneInference.Framework?.CLI).toBeDefined();
|
|
73
|
+
// Operations parent should have sublanes like Infrastructure, CI/CD
|
|
74
|
+
expect(laneInference.Operations?.Infrastructure).toBeDefined();
|
|
75
|
+
expect(laneInference.Operations?.['CI/CD']).toBeDefined();
|
|
76
|
+
// Content parent should have Documentation sublane
|
|
77
|
+
expect(laneInference.Content?.Documentation).toBeDefined();
|
|
78
|
+
});
|
|
79
|
+
it('should have code_paths in sublane config (not patterns)', async () => {
|
|
80
|
+
// Act
|
|
81
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
82
|
+
// Assert
|
|
83
|
+
const laneInference = readLaneInference();
|
|
84
|
+
// Sublanes should have code_paths (not patterns)
|
|
85
|
+
const frameworkCore = laneInference.Framework?.Core;
|
|
86
|
+
expect(frameworkCore).toBeDefined();
|
|
87
|
+
expect(frameworkCore?.code_paths).toBeDefined();
|
|
88
|
+
expect(Array.isArray(frameworkCore?.code_paths)).toBe(true);
|
|
89
|
+
expect(frameworkCore?.patterns).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
it('should include keywords in sublane config', async () => {
|
|
92
|
+
// Act
|
|
93
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
94
|
+
// Assert
|
|
95
|
+
const laneInference = readLaneInference();
|
|
96
|
+
// Sublanes should have keywords array
|
|
97
|
+
const contentDocs = laneInference.Content?.Documentation;
|
|
98
|
+
expect(contentDocs).toBeDefined();
|
|
99
|
+
expect(contentDocs?.keywords).toBeDefined();
|
|
100
|
+
expect(Array.isArray(contentDocs?.keywords)).toBe(true);
|
|
101
|
+
expect(contentDocs?.keywords?.length).toBeGreaterThan(0);
|
|
102
|
+
});
|
|
103
|
+
it('should generate Experience parent lane for frontend projects', async () => {
|
|
104
|
+
// Act
|
|
105
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
106
|
+
// Assert
|
|
107
|
+
const laneInference = readLaneInference();
|
|
108
|
+
// Should have Experience parent for frontend work
|
|
109
|
+
expect(laneInference.Experience).toBeDefined();
|
|
110
|
+
expect(laneInference.Experience?.UI || laneInference.Experience?.Web).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
it('should add framework-specific lanes when --framework is provided', async () => {
|
|
113
|
+
// Act
|
|
114
|
+
await scaffoldProject(tempDir, {
|
|
115
|
+
force: true,
|
|
116
|
+
full: true,
|
|
117
|
+
framework: 'nextjs',
|
|
118
|
+
});
|
|
119
|
+
// Assert
|
|
120
|
+
const laneInference = readLaneInference();
|
|
121
|
+
// Should still have base lanes
|
|
122
|
+
expect(laneInference.Framework).toBeDefined();
|
|
123
|
+
expect(laneInference.Content).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file init-onboarding-docs.test.ts
|
|
3
|
+
* Tests for onboarding docs scaffold (WU-1309)
|
|
4
|
+
* Verifies: starting-prompt, first-15-mins, local-only, lane-inference
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import * as os from 'node:os';
|
|
10
|
+
import { scaffoldProject } from '../init.js';
|
|
11
|
+
describe('onboarding docs scaffold', () => {
|
|
12
|
+
let tempDir;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-onboarding-test-'));
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
// Constants to avoid duplicate strings (sonarjs/no-duplicate-string)
|
|
20
|
+
const ARC42_DOCS_STRUCTURE = 'arc42';
|
|
21
|
+
const STARTING_PROMPT_FILE = 'starting-prompt.md';
|
|
22
|
+
const FIRST_15_MINS_FILE = 'first-15-mins.md';
|
|
23
|
+
const LOCAL_ONLY_FILE = 'local-only.md';
|
|
24
|
+
const LANE_INFERENCE_FILE = 'lane-inference.md';
|
|
25
|
+
function getOnboardingDir(docsStructure = ARC42_DOCS_STRUCTURE) {
|
|
26
|
+
if (docsStructure === 'simple') {
|
|
27
|
+
return path.join(tempDir, 'docs', '_frameworks', 'lumenflow', 'agent', 'onboarding');
|
|
28
|
+
}
|
|
29
|
+
return path.join(tempDir, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding');
|
|
30
|
+
}
|
|
31
|
+
function getArc42Options() {
|
|
32
|
+
return {
|
|
33
|
+
force: false,
|
|
34
|
+
full: true,
|
|
35
|
+
docsStructure: ARC42_DOCS_STRUCTURE,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
describe('required onboarding docs', () => {
|
|
39
|
+
it('should scaffold starting-prompt.md', async () => {
|
|
40
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
41
|
+
const docPath = path.join(getOnboardingDir(), STARTING_PROMPT_FILE);
|
|
42
|
+
expect(fs.existsSync(docPath)).toBe(true);
|
|
43
|
+
const content = fs.readFileSync(docPath, 'utf-8');
|
|
44
|
+
expect(content).toContain('Starting Prompt');
|
|
45
|
+
expect(content).toContain('LUMENFLOW.md');
|
|
46
|
+
});
|
|
47
|
+
it('should scaffold first-15-mins.md', async () => {
|
|
48
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
49
|
+
const docPath = path.join(getOnboardingDir(), FIRST_15_MINS_FILE);
|
|
50
|
+
expect(fs.existsSync(docPath)).toBe(true);
|
|
51
|
+
const content = fs.readFileSync(docPath, 'utf-8');
|
|
52
|
+
expect(content).toContain('First 15 Minutes');
|
|
53
|
+
});
|
|
54
|
+
it('should scaffold local-only.md', async () => {
|
|
55
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
56
|
+
const docPath = path.join(getOnboardingDir(), LOCAL_ONLY_FILE);
|
|
57
|
+
expect(fs.existsSync(docPath)).toBe(true);
|
|
58
|
+
const content = fs.readFileSync(docPath, 'utf-8');
|
|
59
|
+
expect(content).toContain('requireRemote');
|
|
60
|
+
expect(content).toContain('local');
|
|
61
|
+
});
|
|
62
|
+
it('should scaffold lane-inference.md', async () => {
|
|
63
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
64
|
+
const docPath = path.join(getOnboardingDir(), LANE_INFERENCE_FILE);
|
|
65
|
+
expect(fs.existsSync(docPath)).toBe(true);
|
|
66
|
+
const content = fs.readFileSync(docPath, 'utf-8');
|
|
67
|
+
expect(content).toContain('lane');
|
|
68
|
+
expect(content).toContain('.lumenflow.lane-inference.yaml');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('onboarding docs with simple structure', () => {
|
|
72
|
+
it('should scaffold onboarding docs in simple structure', async () => {
|
|
73
|
+
const options = {
|
|
74
|
+
force: false,
|
|
75
|
+
full: true,
|
|
76
|
+
docsStructure: 'simple',
|
|
77
|
+
};
|
|
78
|
+
await scaffoldProject(tempDir, options);
|
|
79
|
+
const onboardingDir = getOnboardingDir('simple');
|
|
80
|
+
expect(fs.existsSync(path.join(onboardingDir, STARTING_PROMPT_FILE))).toBe(true);
|
|
81
|
+
expect(fs.existsSync(path.join(onboardingDir, FIRST_15_MINS_FILE))).toBe(true);
|
|
82
|
+
expect(fs.existsSync(path.join(onboardingDir, LOCAL_ONLY_FILE))).toBe(true);
|
|
83
|
+
expect(fs.existsSync(path.join(onboardingDir, LANE_INFERENCE_FILE))).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('complete onboarding docs set', () => {
|
|
87
|
+
it('should scaffold all required onboarding docs with --full', async () => {
|
|
88
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
89
|
+
const onboardingDir = getOnboardingDir();
|
|
90
|
+
// Required docs from WU-1309
|
|
91
|
+
const requiredDocs = [
|
|
92
|
+
STARTING_PROMPT_FILE,
|
|
93
|
+
FIRST_15_MINS_FILE,
|
|
94
|
+
LOCAL_ONLY_FILE,
|
|
95
|
+
LANE_INFERENCE_FILE,
|
|
96
|
+
// Previously existing docs
|
|
97
|
+
'quick-ref-commands.md',
|
|
98
|
+
'first-wu-mistakes.md',
|
|
99
|
+
'troubleshooting-wu-done.md',
|
|
100
|
+
'agent-safety-card.md',
|
|
101
|
+
'wu-create-checklist.md',
|
|
102
|
+
];
|
|
103
|
+
for (const doc of requiredDocs) {
|
|
104
|
+
const docPath = path.join(onboardingDir, doc);
|
|
105
|
+
expect(fs.existsSync(docPath), `Expected ${doc} to exist`).toBe(true);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
it('should not scaffold onboarding docs without --full', async () => {
|
|
109
|
+
const options = {
|
|
110
|
+
force: false,
|
|
111
|
+
full: false,
|
|
112
|
+
};
|
|
113
|
+
await scaffoldProject(tempDir, options);
|
|
114
|
+
const onboardingDir = getOnboardingDir();
|
|
115
|
+
// Onboarding docs should not exist without --full (unless --client claude)
|
|
116
|
+
expect(fs.existsSync(path.join(onboardingDir, STARTING_PROMPT_FILE))).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('onboarding docs content quality', () => {
|
|
120
|
+
it('should have consistent date placeholder in all docs', async () => {
|
|
121
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
122
|
+
const onboardingDir = getOnboardingDir();
|
|
123
|
+
const docs = fs.readdirSync(onboardingDir).filter((f) => f.endsWith('.md'));
|
|
124
|
+
for (const doc of docs) {
|
|
125
|
+
const content = fs.readFileSync(path.join(onboardingDir, doc), 'utf-8');
|
|
126
|
+
// Should have a date in YYYY-MM-DD format (not {{DATE}} placeholder)
|
|
127
|
+
expect(content).toMatch(/\d{4}-\d{2}-\d{2}/);
|
|
128
|
+
expect(content).not.toContain('{{DATE}}');
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file init-quick-ref.test.ts
|
|
3
|
+
* Tests for quick-ref commands content (WU-1309)
|
|
4
|
+
* Verifies: correct init command, complete wu:create example
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import * as os from 'node:os';
|
|
10
|
+
import { scaffoldProject } from '../init.js';
|
|
11
|
+
// Constants to avoid duplicate strings (sonarjs/no-duplicate-string)
|
|
12
|
+
const ARC42_DOCS_STRUCTURE = 'arc42';
|
|
13
|
+
describe('quick-ref commands', () => {
|
|
14
|
+
let tempDir;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-quickref-test-'));
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
function getQuickRefPath(docsStructure = ARC42_DOCS_STRUCTURE) {
|
|
22
|
+
if (docsStructure === 'simple') {
|
|
23
|
+
return path.join(tempDir, 'docs', '_frameworks', 'lumenflow', 'agent', 'onboarding', 'quick-ref-commands.md');
|
|
24
|
+
}
|
|
25
|
+
return path.join(tempDir, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding', 'quick-ref-commands.md');
|
|
26
|
+
}
|
|
27
|
+
function getArc42Options() {
|
|
28
|
+
return {
|
|
29
|
+
force: false,
|
|
30
|
+
full: true,
|
|
31
|
+
docsStructure: ARC42_DOCS_STRUCTURE,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
describe('init command documentation', () => {
|
|
35
|
+
it('should show correct lumenflow init command', async () => {
|
|
36
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
37
|
+
const quickRefPath = getQuickRefPath();
|
|
38
|
+
const content = fs.readFileSync(quickRefPath, 'utf-8');
|
|
39
|
+
// Should document the init command correctly
|
|
40
|
+
expect(content).toContain('lumenflow init');
|
|
41
|
+
// Should show the various init flags
|
|
42
|
+
expect(content).toContain('--full');
|
|
43
|
+
});
|
|
44
|
+
it('should document --docs-structure flag in quick-ref', async () => {
|
|
45
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
46
|
+
const quickRefPath = getQuickRefPath();
|
|
47
|
+
const content = fs.readFileSync(quickRefPath, 'utf-8');
|
|
48
|
+
// Should document the docs-structure option
|
|
49
|
+
expect(content).toContain('--docs-structure');
|
|
50
|
+
expect(content).toMatch(/simple|arc42/);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('wu:create example', () => {
|
|
54
|
+
it('should include a complete wu:create example with all required fields', async () => {
|
|
55
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
56
|
+
const quickRefPath = getQuickRefPath();
|
|
57
|
+
const content = fs.readFileSync(quickRefPath, 'utf-8');
|
|
58
|
+
// Should have a complete wu:create example
|
|
59
|
+
expect(content).toContain('wu:create');
|
|
60
|
+
expect(content).toContain('--lane');
|
|
61
|
+
expect(content).toContain('--title');
|
|
62
|
+
expect(content).toContain('--description');
|
|
63
|
+
expect(content).toContain('--acceptance');
|
|
64
|
+
expect(content).toContain('--code-paths');
|
|
65
|
+
expect(content).toContain('--exposure');
|
|
66
|
+
});
|
|
67
|
+
it('should show test-paths in wu:create example', async () => {
|
|
68
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
69
|
+
const quickRefPath = getQuickRefPath();
|
|
70
|
+
const content = fs.readFileSync(quickRefPath, 'utf-8');
|
|
71
|
+
// Should include test paths
|
|
72
|
+
expect(content).toMatch(/--test-paths-(unit|e2e)/);
|
|
73
|
+
});
|
|
74
|
+
it('should show spec-refs in wu:create example for feature WUs', async () => {
|
|
75
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
76
|
+
const quickRefPath = getQuickRefPath();
|
|
77
|
+
const content = fs.readFileSync(quickRefPath, 'utf-8');
|
|
78
|
+
// Should include spec-refs for feature WUs
|
|
79
|
+
expect(content).toContain('--spec-refs');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('AGENTS.md quick-ref link', () => {
|
|
83
|
+
it('should have correct quick-ref link for arc42 structure', async () => {
|
|
84
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
85
|
+
const agentsContent = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
86
|
+
// Should point to arc42 path
|
|
87
|
+
expect(agentsContent).toContain('docs/04-operations/_frameworks/lumenflow/agent/onboarding/quick-ref-commands.md');
|
|
88
|
+
});
|
|
89
|
+
it('should have correct quick-ref link for simple structure', async () => {
|
|
90
|
+
const options = {
|
|
91
|
+
force: false,
|
|
92
|
+
full: true,
|
|
93
|
+
docsStructure: 'simple',
|
|
94
|
+
};
|
|
95
|
+
await scaffoldProject(tempDir, options);
|
|
96
|
+
const agentsContent = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
97
|
+
// Should point to simple path (without 04-operations)
|
|
98
|
+
expect(agentsContent).toContain('docs/_frameworks/lumenflow/agent/onboarding/quick-ref-commands.md');
|
|
99
|
+
expect(agentsContent).not.toContain('04-operations');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('quick-ref command tables', () => {
|
|
103
|
+
it('should have project setup commands including init', async () => {
|
|
104
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
105
|
+
const quickRefPath = getQuickRefPath();
|
|
106
|
+
const content = fs.readFileSync(quickRefPath, 'utf-8');
|
|
107
|
+
// Should have a project setup section
|
|
108
|
+
// eslint-disable-next-line sonarjs/slow-regex -- Simple alternation, no backtracking risk
|
|
109
|
+
expect(content).toMatch(/##.*?Setup|##.*?Project/i);
|
|
110
|
+
expect(content).toContain('lumenflow init');
|
|
111
|
+
});
|
|
112
|
+
it('should have WU management commands', async () => {
|
|
113
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
114
|
+
const quickRefPath = getQuickRefPath();
|
|
115
|
+
const content = fs.readFileSync(quickRefPath, 'utf-8');
|
|
116
|
+
// Should have WU commands
|
|
117
|
+
expect(content).toContain('wu:create');
|
|
118
|
+
expect(content).toContain('wu:claim');
|
|
119
|
+
expect(content).toContain('wu:done');
|
|
120
|
+
expect(content).toContain('wu:block');
|
|
121
|
+
});
|
|
122
|
+
it('should have gates commands', async () => {
|
|
123
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
124
|
+
const quickRefPath = getQuickRefPath();
|
|
125
|
+
const content = fs.readFileSync(quickRefPath, 'utf-8');
|
|
126
|
+
// Should have gates commands
|
|
127
|
+
expect(content).toContain('pnpm gates');
|
|
128
|
+
expect(content).toContain('--docs-only');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('workflow sequence', () => {
|
|
132
|
+
it('should have a complete workflow sequence example', async () => {
|
|
133
|
+
await scaffoldProject(tempDir, getArc42Options());
|
|
134
|
+
const quickRefPath = getQuickRefPath();
|
|
135
|
+
const content = fs.readFileSync(quickRefPath, 'utf-8');
|
|
136
|
+
// Should have workflow sequence
|
|
137
|
+
expect(content).toMatch(/workflow|sequence/i);
|
|
138
|
+
// Should show the full flow: create -> claim -> work -> commit -> gates -> done
|
|
139
|
+
expect(content).toContain('wu:create');
|
|
140
|
+
expect(content).toContain('wu:claim');
|
|
141
|
+
expect(content).toContain('pnpm gates');
|
|
142
|
+
expect(content).toContain('wu:done');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|