popeye-cli 1.4.7 → 1.6.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/CHANGELOG.md +54 -0
- package/README.md +264 -63
- package/dist/adapters/gemini.d.ts +1 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +9 -4
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/grok.d.ts +1 -0
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +9 -4
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +35 -9
- package/dist/adapters/openai.js.map +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +54 -4
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts +29 -0
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +132 -7
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts +8 -2
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +37 -316
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +64 -0
- package/dist/generators/doc-parser.d.ts.map +1 -0
- package/dist/generators/doc-parser.js +407 -0
- package/dist/generators/doc-parser.js.map +1 -0
- package/dist/generators/frontend-design-analyzer.d.ts +30 -0
- package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
- package/dist/generators/frontend-design-analyzer.js +208 -0
- package/dist/generators/frontend-design-analyzer.js.map +1 -0
- package/dist/generators/shared-packages.d.ts +45 -0
- package/dist/generators/shared-packages.d.ts.map +1 -0
- package/dist/generators/shared-packages.js +456 -0
- package/dist/generators/shared-packages.js.map +1 -0
- package/dist/generators/templates/index.d.ts +8 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +8 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/generators/templates/website-components.d.ts +33 -0
- package/dist/generators/templates/website-components.d.ts.map +1 -0
- package/dist/generators/templates/website-components.js +303 -0
- package/dist/generators/templates/website-components.js.map +1 -0
- package/dist/generators/templates/website-config.d.ts +55 -0
- package/dist/generators/templates/website-config.d.ts.map +1 -0
- package/dist/generators/templates/website-config.js +425 -0
- package/dist/generators/templates/website-config.js.map +1 -0
- package/dist/generators/templates/website-conversion.d.ts +27 -0
- package/dist/generators/templates/website-conversion.d.ts.map +1 -0
- package/dist/generators/templates/website-conversion.js +326 -0
- package/dist/generators/templates/website-conversion.js.map +1 -0
- package/dist/generators/templates/website-landing.d.ts +24 -0
- package/dist/generators/templates/website-landing.d.ts.map +1 -0
- package/dist/generators/templates/website-landing.js +276 -0
- package/dist/generators/templates/website-landing.js.map +1 -0
- package/dist/generators/templates/website-layout.d.ts +42 -0
- package/dist/generators/templates/website-layout.d.ts.map +1 -0
- package/dist/generators/templates/website-layout.js +408 -0
- package/dist/generators/templates/website-layout.js.map +1 -0
- package/dist/generators/templates/website-pricing.d.ts +11 -0
- package/dist/generators/templates/website-pricing.d.ts.map +1 -0
- package/dist/generators/templates/website-pricing.js +313 -0
- package/dist/generators/templates/website-pricing.js.map +1 -0
- package/dist/generators/templates/website-sections.d.ts +102 -0
- package/dist/generators/templates/website-sections.d.ts.map +1 -0
- package/dist/generators/templates/website-sections.js +444 -0
- package/dist/generators/templates/website-sections.js.map +1 -0
- package/dist/generators/templates/website-seo.d.ts +76 -0
- package/dist/generators/templates/website-seo.d.ts.map +1 -0
- package/dist/generators/templates/website-seo.js +326 -0
- package/dist/generators/templates/website-seo.js.map +1 -0
- package/dist/generators/templates/website.d.ts +10 -83
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +12 -875
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-content-scanner.d.ts +37 -0
- package/dist/generators/website-content-scanner.d.ts.map +1 -0
- package/dist/generators/website-content-scanner.js +165 -0
- package/dist/generators/website-content-scanner.js.map +1 -0
- package/dist/generators/website-context.d.ts +119 -0
- package/dist/generators/website-context.d.ts.map +1 -0
- package/dist/generators/website-context.js +350 -0
- package/dist/generators/website-context.js.map +1 -0
- package/dist/generators/website-debug.d.ts +68 -0
- package/dist/generators/website-debug.d.ts.map +1 -0
- package/dist/generators/website-debug.js +93 -0
- package/dist/generators/website-debug.js.map +1 -0
- package/dist/generators/website.d.ts +5 -0
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +136 -11
- package/dist/generators/website.js.map +1 -1
- package/dist/generators/workspace-root.d.ts +27 -0
- package/dist/generators/workspace-root.d.ts.map +1 -0
- package/dist/generators/workspace-root.js +100 -0
- package/dist/generators/workspace-root.js.map +1 -0
- package/dist/state/index.d.ts +35 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +40 -0
- package/dist/state/index.js.map +1 -1
- package/dist/types/consensus.d.ts +3 -0
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +1 -0
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/website-strategy.d.ts +263 -0
- package/dist/types/website-strategy.d.ts.map +1 -0
- package/dist/types/website-strategy.js +105 -0
- package/dist/types/website-strategy.js.map +1 -0
- package/dist/types/workflow.d.ts +21 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +8 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.d.ts +15 -0
- package/dist/upgrade/handlers.d.ts.map +1 -1
- package/dist/upgrade/handlers.js +52 -0
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/auto-fix-bundler.d.ts +37 -0
- package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
- package/dist/workflow/auto-fix-bundler.js +320 -0
- package/dist/workflow/auto-fix-bundler.js.map +1 -0
- package/dist/workflow/auto-fix.d.ts.map +1 -1
- package/dist/workflow/auto-fix.js +10 -3
- package/dist/workflow/auto-fix.js.map +1 -1
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +2 -0
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +18 -0
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +4 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +37 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/overview.d.ts +89 -0
- package/dist/workflow/overview.d.ts.map +1 -0
- package/dist/workflow/overview.js +358 -0
- package/dist/workflow/overview.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +6 -4
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +148 -6
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts +79 -0
- package/dist/workflow/website-strategy.d.ts.map +1 -0
- package/dist/workflow/website-strategy.js +310 -0
- package/dist/workflow/website-strategy.js.map +1 -0
- package/dist/workflow/website-updater.d.ts +17 -0
- package/dist/workflow/website-updater.d.ts.map +1 -0
- package/dist/workflow/website-updater.js +116 -0
- package/dist/workflow/website-updater.js.map +1 -0
- package/dist/workflow/workflow-logger.d.ts +1 -1
- package/dist/workflow/workflow-logger.d.ts.map +1 -1
- package/dist/workflow/workflow-logger.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/gemini.ts +10 -4
- package/src/adapters/grok.ts +10 -4
- package/src/adapters/openai.ts +38 -6
- package/src/cli/commands/create.ts +58 -4
- package/src/cli/interactive.ts +143 -7
- package/src/generators/all.ts +49 -332
- package/src/generators/doc-parser.ts +449 -0
- package/src/generators/frontend-design-analyzer.ts +261 -0
- package/src/generators/shared-packages.ts +500 -0
- package/src/generators/templates/index.ts +8 -0
- package/src/generators/templates/website-components.ts +330 -0
- package/src/generators/templates/website-config.ts +444 -0
- package/src/generators/templates/website-conversion.ts +341 -0
- package/src/generators/templates/website-landing.ts +331 -0
- package/src/generators/templates/website-layout.ts +443 -0
- package/src/generators/templates/website-pricing.ts +330 -0
- package/src/generators/templates/website-sections.ts +541 -0
- package/src/generators/templates/website-seo.ts +370 -0
- package/src/generators/templates/website.ts +38 -905
- package/src/generators/website-content-scanner.ts +208 -0
- package/src/generators/website-context.ts +493 -0
- package/src/generators/website-debug.ts +130 -0
- package/src/generators/website.ts +178 -20
- package/src/generators/workspace-root.ts +113 -0
- package/src/state/index.ts +56 -0
- package/src/types/consensus.ts +3 -0
- package/src/types/website-strategy.ts +243 -0
- package/src/types/workflow.ts +21 -0
- package/src/upgrade/handlers.ts +65 -0
- package/src/workflow/auto-fix-bundler.ts +392 -0
- package/src/workflow/auto-fix.ts +11 -3
- package/src/workflow/consensus.ts +2 -0
- package/src/workflow/execution-mode.ts +21 -0
- package/src/workflow/index.ts +37 -0
- package/src/workflow/overview.ts +475 -0
- package/src/workflow/plan-mode.ts +193 -8
- package/src/workflow/website-strategy.ts +379 -0
- package/src/workflow/website-updater.ts +142 -0
- package/src/workflow/workflow-logger.ts +1 -0
- package/tests/adapters/persona-switching.test.ts +63 -0
- package/tests/cli/project-naming.test.ts +136 -0
- package/tests/generators/doc-parser.test.ts +121 -0
- package/tests/generators/frontend-design-analyzer.test.ts +90 -0
- package/tests/generators/quality-gate.test.ts +183 -0
- package/tests/generators/shared-packages.test.ts +83 -0
- package/tests/generators/website-components.test.ts +159 -0
- package/tests/generators/website-config.test.ts +84 -0
- package/tests/generators/website-content-scanner.test.ts +181 -0
- package/tests/generators/website-context.test.ts +331 -0
- package/tests/generators/website-debug.test.ts +77 -0
- package/tests/generators/website-landing.test.ts +188 -0
- package/tests/generators/website-pricing.test.ts +98 -0
- package/tests/generators/website-sections.test.ts +245 -0
- package/tests/generators/website-seo-quality.test.ts +246 -0
- package/tests/generators/workspace-root.test.ts +105 -0
- package/tests/upgrade/handlers.test.ts +162 -0
- package/tests/workflow/auto-fix-bundler.test.ts +242 -0
- package/tests/workflow/overview.test.ts +392 -0
- package/tests/workflow/plan-mode.test.ts +111 -1
- package/tests/workflow/website-strategy.test.ts +246 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for overview module
|
|
3
|
+
* Verifies overview generation, formatting, analysis, and fix capabilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import { generateOverview, formatOverview, fixOverviewIssues } from '../../src/workflow/overview.js';
|
|
11
|
+
import type { ProjectOverview, OverviewFixResult } from '../../src/workflow/overview.js';
|
|
12
|
+
|
|
13
|
+
// Mock the state module to avoid filesystem coupling in unit tests
|
|
14
|
+
vi.mock('../../src/state/index.js', async () => {
|
|
15
|
+
const actual = await vi.importActual('../../src/state/index.js');
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
loadProject: vi.fn(),
|
|
19
|
+
getProgress: vi.fn(),
|
|
20
|
+
storeUserDocs: vi.fn(),
|
|
21
|
+
storeBrandContext: vi.fn(),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Mock website-context to control doc discovery
|
|
26
|
+
vi.mock('../../src/generators/website-context.js', async () => {
|
|
27
|
+
const actual = await vi.importActual('../../src/generators/website-context.js');
|
|
28
|
+
return {
|
|
29
|
+
...actual,
|
|
30
|
+
discoverProjectDocs: vi.fn().mockResolvedValue([]),
|
|
31
|
+
readProjectDocs: vi.fn().mockResolvedValue(''),
|
|
32
|
+
findBrandAssets: vi.fn().mockResolvedValue({}),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Mock website-updater
|
|
37
|
+
vi.mock('../../src/workflow/website-updater.js', () => ({
|
|
38
|
+
updateWebsiteContent: vi.fn().mockResolvedValue(undefined),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
import { loadProject, getProgress, storeUserDocs, storeBrandContext } from '../../src/state/index.js';
|
|
42
|
+
import { discoverProjectDocs, readProjectDocs, findBrandAssets } from '../../src/generators/website-context.js';
|
|
43
|
+
|
|
44
|
+
const mockLoadProject = vi.mocked(loadProject);
|
|
45
|
+
const mockGetProgress = vi.mocked(getProgress);
|
|
46
|
+
const mockStoreUserDocs = vi.mocked(storeUserDocs);
|
|
47
|
+
const mockStoreBrandContext = vi.mocked(storeBrandContext);
|
|
48
|
+
const mockDiscoverDocs = vi.mocked(discoverProjectDocs);
|
|
49
|
+
const mockReadDocs = vi.mocked(readProjectDocs);
|
|
50
|
+
const mockFindBrand = vi.mocked(findBrandAssets);
|
|
51
|
+
|
|
52
|
+
const baseState = {
|
|
53
|
+
id: 'test-id',
|
|
54
|
+
name: 'test-project',
|
|
55
|
+
idea: 'A test project',
|
|
56
|
+
language: 'typescript' as const,
|
|
57
|
+
openaiModel: 'gpt-4o',
|
|
58
|
+
phase: 'execution' as const,
|
|
59
|
+
status: 'in-progress' as const,
|
|
60
|
+
specification: '# Overview\nA great project for testing.\n## Core Features\n- Feature A\n- Feature B',
|
|
61
|
+
plan: '# Plan\n...',
|
|
62
|
+
milestones: [
|
|
63
|
+
{
|
|
64
|
+
id: 'm1',
|
|
65
|
+
name: 'Setup',
|
|
66
|
+
description: 'Initial setup',
|
|
67
|
+
status: 'complete' as const,
|
|
68
|
+
tasks: [
|
|
69
|
+
{ id: 't1', name: 'Init project', description: 'Initialize', status: 'complete' as const },
|
|
70
|
+
{ id: 't2', name: 'Add config', description: 'Configuration', status: 'complete' as const },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'm2',
|
|
75
|
+
name: 'Core Features',
|
|
76
|
+
description: 'Build core features',
|
|
77
|
+
status: 'in-progress' as const,
|
|
78
|
+
tasks: [
|
|
79
|
+
{ id: 't3', name: 'Build API', description: 'API endpoints', status: 'complete' as const },
|
|
80
|
+
{ id: 't4', name: 'Build UI', description: 'User interface', status: 'pending' as const },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
currentMilestone: 'm2',
|
|
85
|
+
currentTask: 't4',
|
|
86
|
+
consensusHistory: [],
|
|
87
|
+
createdAt: '2024-01-01',
|
|
88
|
+
updatedAt: '2024-01-02',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
describe('generateOverview', () => {
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
vi.clearAllMocks();
|
|
94
|
+
mockDiscoverDocs.mockResolvedValue([]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('reads state and produces correct structure', async () => {
|
|
98
|
+
mockLoadProject.mockResolvedValue({
|
|
99
|
+
...baseState,
|
|
100
|
+
userDocs: '--- spec.md ---\nSome docs',
|
|
101
|
+
brandContext: { primaryColor: '#2563EB' },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
mockGetProgress.mockResolvedValue({
|
|
105
|
+
totalMilestones: 2,
|
|
106
|
+
completedMilestones: 1,
|
|
107
|
+
totalTasks: 4,
|
|
108
|
+
completedTasks: 3,
|
|
109
|
+
percentComplete: 75,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const overview = await generateOverview('/fake/project/dir');
|
|
113
|
+
|
|
114
|
+
expect(overview.name).toBe('test-project');
|
|
115
|
+
expect(overview.language).toBe('typescript');
|
|
116
|
+
expect(overview.phase).toBe('execution');
|
|
117
|
+
expect(overview.plan.totalMilestones).toBe(2);
|
|
118
|
+
expect(overview.plan.totalTasks).toBe(4);
|
|
119
|
+
expect(overview.progress.percentComplete).toBe(75);
|
|
120
|
+
expect(overview.progress.completedTasks).toBe(3);
|
|
121
|
+
expect(overview.userDocs).toEqual(['spec.md']);
|
|
122
|
+
expect(overview.brandContext?.primaryColor).toBe('#2563EB');
|
|
123
|
+
expect(overview.specification.keyFeatures).toContain('Feature A');
|
|
124
|
+
expect(overview.specification.keyFeatures).toContain('Feature B');
|
|
125
|
+
expect(overview.issues).toBeDefined();
|
|
126
|
+
expect(overview.availableDocs).toBeDefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('detects missing docs when docs available in CWD', async () => {
|
|
130
|
+
mockLoadProject.mockResolvedValue({ ...baseState });
|
|
131
|
+
mockGetProgress.mockResolvedValue({
|
|
132
|
+
totalMilestones: 2, completedMilestones: 1,
|
|
133
|
+
totalTasks: 4, completedTasks: 3, percentComplete: 75,
|
|
134
|
+
});
|
|
135
|
+
mockDiscoverDocs.mockResolvedValue(['/fake/spec.md', '/fake/pricing.md']);
|
|
136
|
+
|
|
137
|
+
const overview = await generateOverview('/fake/project/dir');
|
|
138
|
+
|
|
139
|
+
expect(overview.availableDocs).toEqual(['spec.md', 'pricing.md']);
|
|
140
|
+
const docsIssue = overview.issues.find((i) => i.category === 'docs');
|
|
141
|
+
expect(docsIssue).toBeDefined();
|
|
142
|
+
expect(docsIssue?.message).toContain('2 doc(s)');
|
|
143
|
+
expect(docsIssue?.fix).toContain('/overview fix');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('detects generic specification', async () => {
|
|
147
|
+
mockLoadProject.mockResolvedValue({
|
|
148
|
+
...baseState,
|
|
149
|
+
specification: "Here's a comprehensive software specification for your project...",
|
|
150
|
+
});
|
|
151
|
+
mockGetProgress.mockResolvedValue({
|
|
152
|
+
totalMilestones: 2, completedMilestones: 1,
|
|
153
|
+
totalTasks: 4, completedTasks: 3, percentComplete: 75,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const overview = await generateOverview('/fake/project/dir');
|
|
157
|
+
|
|
158
|
+
const specIssue = overview.issues.find((i) => i.category === 'spec');
|
|
159
|
+
expect(specIssue).toBeDefined();
|
|
160
|
+
expect(specIssue?.message).toContain('generic AI-generated');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('detects no brand context', async () => {
|
|
164
|
+
mockLoadProject.mockResolvedValue({ ...baseState });
|
|
165
|
+
mockGetProgress.mockResolvedValue({
|
|
166
|
+
totalMilestones: 2, completedMilestones: 1,
|
|
167
|
+
totalTasks: 4, completedTasks: 3, percentComplete: 75,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const overview = await generateOverview('/fake/project/dir');
|
|
171
|
+
|
|
172
|
+
const brandIssue = overview.issues.find((i) => i.category === 'brand');
|
|
173
|
+
expect(brandIssue).toBeDefined();
|
|
174
|
+
expect(brandIssue?.message).toContain('No brand context');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('detects very few tasks', async () => {
|
|
178
|
+
mockLoadProject.mockResolvedValue({
|
|
179
|
+
...baseState,
|
|
180
|
+
milestones: [{
|
|
181
|
+
id: 'm1', name: 'Only', description: 'Only milestone',
|
|
182
|
+
status: 'pending',
|
|
183
|
+
tasks: [{ id: 't1', name: 'Only task', description: 'Only', status: 'pending' }],
|
|
184
|
+
}],
|
|
185
|
+
});
|
|
186
|
+
mockGetProgress.mockResolvedValue({
|
|
187
|
+
totalMilestones: 1, completedMilestones: 0,
|
|
188
|
+
totalTasks: 1, completedTasks: 0, percentComplete: 0,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const overview = await generateOverview('/fake/project/dir');
|
|
192
|
+
|
|
193
|
+
const planIssue = overview.issues.find((i) => i.category === 'plan');
|
|
194
|
+
expect(planIssue).toBeDefined();
|
|
195
|
+
expect(planIssue?.message).toContain('only 1 task(s)');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('formatOverview', () => {
|
|
200
|
+
it('produces formatted string with progress info', () => {
|
|
201
|
+
const overview: ProjectOverview = {
|
|
202
|
+
name: 'my-project',
|
|
203
|
+
idea: 'A test idea',
|
|
204
|
+
language: 'typescript',
|
|
205
|
+
phase: 'execution',
|
|
206
|
+
status: 'in-progress',
|
|
207
|
+
specification: {
|
|
208
|
+
summary: 'A great project',
|
|
209
|
+
keyFeatures: ['Feature A', 'Feature B'],
|
|
210
|
+
},
|
|
211
|
+
plan: {
|
|
212
|
+
totalMilestones: 2,
|
|
213
|
+
totalTasks: 4,
|
|
214
|
+
milestones: [
|
|
215
|
+
{
|
|
216
|
+
name: 'Setup',
|
|
217
|
+
status: 'complete',
|
|
218
|
+
taskCount: 2,
|
|
219
|
+
completedTasks: 2,
|
|
220
|
+
tasks: [
|
|
221
|
+
{ name: 'Init', status: 'complete' },
|
|
222
|
+
{ name: 'Config', status: 'complete' },
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: 'Core',
|
|
227
|
+
status: 'in-progress',
|
|
228
|
+
taskCount: 2,
|
|
229
|
+
completedTasks: 1,
|
|
230
|
+
tasks: [
|
|
231
|
+
{ name: 'API', status: 'complete' },
|
|
232
|
+
{ name: 'UI', status: 'pending' },
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
progress: {
|
|
238
|
+
completedMilestones: 1,
|
|
239
|
+
completedTasks: 3,
|
|
240
|
+
percentComplete: 75,
|
|
241
|
+
},
|
|
242
|
+
issues: [],
|
|
243
|
+
availableDocs: [],
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const output = formatOverview(overview);
|
|
247
|
+
|
|
248
|
+
expect(output).toContain('my-project');
|
|
249
|
+
expect(output).toContain('75%');
|
|
250
|
+
expect(output).toContain('3/4 tasks');
|
|
251
|
+
expect(output).toContain('Setup');
|
|
252
|
+
expect(output).toContain('Core');
|
|
253
|
+
expect(output).toContain('[x]'); // complete status
|
|
254
|
+
expect(output).toContain('[ ]'); // pending status
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('handles project with no milestones', () => {
|
|
258
|
+
const overview: ProjectOverview = {
|
|
259
|
+
name: 'empty-project',
|
|
260
|
+
idea: 'Nothing yet',
|
|
261
|
+
language: 'python',
|
|
262
|
+
phase: 'plan',
|
|
263
|
+
status: 'pending',
|
|
264
|
+
specification: {
|
|
265
|
+
summary: '',
|
|
266
|
+
keyFeatures: [],
|
|
267
|
+
},
|
|
268
|
+
plan: {
|
|
269
|
+
totalMilestones: 0,
|
|
270
|
+
totalTasks: 0,
|
|
271
|
+
milestones: [],
|
|
272
|
+
},
|
|
273
|
+
progress: {
|
|
274
|
+
completedMilestones: 0,
|
|
275
|
+
completedTasks: 0,
|
|
276
|
+
percentComplete: 0,
|
|
277
|
+
},
|
|
278
|
+
issues: [],
|
|
279
|
+
availableDocs: [],
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const output = formatOverview(overview);
|
|
283
|
+
|
|
284
|
+
expect(output).toContain('empty-project');
|
|
285
|
+
expect(output).toContain('0%');
|
|
286
|
+
expect(output).toContain('No milestones defined yet');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('shows analysis section when issues exist', () => {
|
|
290
|
+
const overview: ProjectOverview = {
|
|
291
|
+
name: 'test-project',
|
|
292
|
+
idea: 'Test',
|
|
293
|
+
language: 'website',
|
|
294
|
+
phase: 'complete',
|
|
295
|
+
status: 'complete',
|
|
296
|
+
specification: { summary: 'Test', keyFeatures: [] },
|
|
297
|
+
plan: { totalMilestones: 1, totalTasks: 5, milestones: [] },
|
|
298
|
+
progress: { completedMilestones: 1, completedTasks: 5, percentComplete: 100 },
|
|
299
|
+
issues: [
|
|
300
|
+
{ severity: 'warning', category: 'docs', message: 'No docs found', fix: 'Add docs' },
|
|
301
|
+
{ severity: 'error', category: 'website', message: 'Placeholder content', fix: 'Run /overview fix' },
|
|
302
|
+
],
|
|
303
|
+
availableDocs: ['spec.md'],
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const output = formatOverview(overview);
|
|
307
|
+
|
|
308
|
+
expect(output).toContain('ANALYSIS');
|
|
309
|
+
expect(output).toContain('[!] DOCS: No docs found');
|
|
310
|
+
expect(output).toContain('[!!] WEBSITE: Placeholder content');
|
|
311
|
+
expect(output).toContain('-> Add docs');
|
|
312
|
+
expect(output).toContain('-> Run /overview fix');
|
|
313
|
+
expect(output).toContain('/overview fix');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('shows available docs section when docs not yet imported', () => {
|
|
317
|
+
const overview: ProjectOverview = {
|
|
318
|
+
name: 'test-project',
|
|
319
|
+
idea: 'Test',
|
|
320
|
+
language: 'typescript',
|
|
321
|
+
phase: 'execution',
|
|
322
|
+
status: 'in-progress',
|
|
323
|
+
specification: { summary: 'Test', keyFeatures: [] },
|
|
324
|
+
plan: { totalMilestones: 1, totalTasks: 5, milestones: [] },
|
|
325
|
+
progress: { completedMilestones: 0, completedTasks: 2, percentComplete: 40 },
|
|
326
|
+
issues: [],
|
|
327
|
+
availableDocs: ['spec.md', 'pricing.md'],
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const output = formatOverview(overview);
|
|
331
|
+
|
|
332
|
+
expect(output).toContain('AVAILABLE DOCS (not yet imported)');
|
|
333
|
+
expect(output).toContain('spec.md');
|
|
334
|
+
expect(output).toContain('pricing.md');
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('fixOverviewIssues', () => {
|
|
339
|
+
beforeEach(() => {
|
|
340
|
+
vi.clearAllMocks();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('discovers docs and stores them', async () => {
|
|
344
|
+
const stateWithDocs = { ...baseState, userDocs: '--- spec.md ---\nContent' };
|
|
345
|
+
mockLoadProject.mockResolvedValue({ ...baseState });
|
|
346
|
+
mockDiscoverDocs.mockResolvedValue(['/parent/spec.md']);
|
|
347
|
+
mockReadDocs.mockResolvedValue('--- spec.md ---\nSpec content');
|
|
348
|
+
mockFindBrand.mockResolvedValue({});
|
|
349
|
+
mockStoreUserDocs.mockResolvedValue(stateWithDocs);
|
|
350
|
+
|
|
351
|
+
const result = await fixOverviewIssues('/parent/project/dir');
|
|
352
|
+
|
|
353
|
+
expect(result.docsDiscovered).toBe(1);
|
|
354
|
+
expect(result.docsStored).toBe(true);
|
|
355
|
+
expect(result.messages.some((m) => m.includes('spec.md'))).toBe(true);
|
|
356
|
+
expect(mockStoreUserDocs).toHaveBeenCalledWith('/parent/project/dir', '--- spec.md ---\nSpec content');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('finds brand assets and extracts colors', async () => {
|
|
360
|
+
const stateWithBrand = {
|
|
361
|
+
...baseState,
|
|
362
|
+
userDocs: '# Colors\nPrimary: #2563EB',
|
|
363
|
+
brandContext: { logoPath: '/parent/logo.png', primaryColor: '#2563EB' },
|
|
364
|
+
};
|
|
365
|
+
mockLoadProject.mockResolvedValue({ ...baseState });
|
|
366
|
+
mockDiscoverDocs.mockResolvedValue(['/parent/colors.md']);
|
|
367
|
+
mockReadDocs.mockResolvedValue('# Colors\nPrimary: #2563EB');
|
|
368
|
+
mockFindBrand.mockResolvedValue({ logoPath: '/parent/logo.png' });
|
|
369
|
+
mockStoreUserDocs.mockResolvedValue({ ...baseState, userDocs: '# Colors\nPrimary: #2563EB' });
|
|
370
|
+
mockStoreBrandContext
|
|
371
|
+
.mockResolvedValueOnce({ ...baseState, brandContext: { logoPath: '/parent/logo.png' }, userDocs: '# Colors\nPrimary: #2563EB' })
|
|
372
|
+
.mockResolvedValueOnce(stateWithBrand);
|
|
373
|
+
|
|
374
|
+
const result = await fixOverviewIssues('/parent/project/dir');
|
|
375
|
+
|
|
376
|
+
expect(result.brandFound).toBe(true);
|
|
377
|
+
expect(result.messages.some((m) => m.includes('logo.png'))).toBe(true);
|
|
378
|
+
expect(result.messages.some((m) => m.includes('#2563EB'))).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('shows tip when no docs or brand found', async () => {
|
|
382
|
+
mockLoadProject.mockResolvedValue({ ...baseState });
|
|
383
|
+
mockDiscoverDocs.mockResolvedValue([]);
|
|
384
|
+
mockFindBrand.mockResolvedValue({});
|
|
385
|
+
|
|
386
|
+
const result = await fixOverviewIssues('/parent/project/dir');
|
|
387
|
+
|
|
388
|
+
expect(result.docsDiscovered).toBe(0);
|
|
389
|
+
expect(result.brandFound).toBe(false);
|
|
390
|
+
expect(result.messages.some((m) => m.includes('Tip:'))).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import { parsePlanMilestones } from '../../src/workflow/plan-mode.js';
|
|
6
|
+
import { parsePlanMilestones, parseTaskTag, tagToAppTarget } from '../../src/workflow/plan-mode.js';
|
|
7
7
|
|
|
8
8
|
describe('parsePlanMilestones', () => {
|
|
9
9
|
describe('with explicit task markers', () => {
|
|
@@ -195,6 +195,76 @@ Description: Build the main API endpoints
|
|
|
195
195
|
});
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
+
describe('app tag handling', () => {
|
|
199
|
+
it('should extract tasks with [WEB] tags', () => {
|
|
200
|
+
const plan = `
|
|
201
|
+
## Milestone 1: Website Branding
|
|
202
|
+
|
|
203
|
+
### Task 1.1 [WEB]: Update root layout with Gateco branding
|
|
204
|
+
- **Description**: Replace default branding with Gateco colors and logo
|
|
205
|
+
|
|
206
|
+
### Task 1.2 [WEB]: Create hero section component
|
|
207
|
+
- **Description**: Build the landing page hero with CTA
|
|
208
|
+
`;
|
|
209
|
+
|
|
210
|
+
const milestones = parsePlanMilestones(plan);
|
|
211
|
+
const allTasks = milestones.flatMap(m => m.tasks);
|
|
212
|
+
|
|
213
|
+
expect(allTasks.length).toBeGreaterThanOrEqual(2);
|
|
214
|
+
const taskNames = allTasks.map(t => t.name.toLowerCase());
|
|
215
|
+
expect(taskNames.some(n => n.includes('update root layout'))).toBe(true);
|
|
216
|
+
expect(taskNames.some(n => n.includes('create hero section'))).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should extract tasks with [INT] tags', () => {
|
|
220
|
+
const plan = `
|
|
221
|
+
## Milestone 3: Integration
|
|
222
|
+
|
|
223
|
+
### Task 3.1 [INT]: Wire frontend auth to backend API
|
|
224
|
+
- **Description**: Connect the frontend login form to the backend auth endpoint
|
|
225
|
+
|
|
226
|
+
### Task 3.2 [INT]: Set up end-to-end test suite
|
|
227
|
+
- **Description**: Create E2E tests covering the full auth flow
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
const milestones = parsePlanMilestones(plan);
|
|
231
|
+
const allTasks = milestones.flatMap(m => m.tasks);
|
|
232
|
+
|
|
233
|
+
expect(allTasks.length).toBeGreaterThanOrEqual(2);
|
|
234
|
+
const taskNames = allTasks.map(t => t.name.toLowerCase());
|
|
235
|
+
expect(taskNames.some(n => n.includes('wire frontend auth'))).toBe(true);
|
|
236
|
+
expect(taskNames.some(n => n.includes('set up end-to-end'))).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should extract tasks with mixed app tags across milestones', () => {
|
|
240
|
+
const plan = `
|
|
241
|
+
## Milestone 1: Setup
|
|
242
|
+
|
|
243
|
+
### Task 1.1 [FE]: Create React component library
|
|
244
|
+
- **Description**: Scaffold shared components
|
|
245
|
+
|
|
246
|
+
### Task 1.2 [BE]: Implement REST API endpoints
|
|
247
|
+
- **Description**: Build core API
|
|
248
|
+
|
|
249
|
+
### Task 1.3 [WEB]: Build marketing landing page
|
|
250
|
+
- **Description**: Create the public-facing website
|
|
251
|
+
|
|
252
|
+
### Task 1.4 [INT]: Configure CI/CD pipeline
|
|
253
|
+
- **Description**: Set up automated deployment
|
|
254
|
+
`;
|
|
255
|
+
|
|
256
|
+
const milestones = parsePlanMilestones(plan);
|
|
257
|
+
const allTasks = milestones.flatMap(m => m.tasks);
|
|
258
|
+
|
|
259
|
+
expect(allTasks.length).toBeGreaterThanOrEqual(4);
|
|
260
|
+
const taskNames = allTasks.map(t => t.name.toLowerCase());
|
|
261
|
+
expect(taskNames.some(n => n.includes('create react component'))).toBe(true);
|
|
262
|
+
expect(taskNames.some(n => n.includes('implement rest api'))).toBe(true);
|
|
263
|
+
expect(taskNames.some(n => n.includes('build marketing landing'))).toBe(true);
|
|
264
|
+
expect(taskNames.some(n => n.includes('configure ci/cd'))).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
198
268
|
describe('fallback behavior', () => {
|
|
199
269
|
it('should create a default milestone when no tasks found', () => {
|
|
200
270
|
const plan = `
|
|
@@ -211,3 +281,43 @@ The project will do various things but no specific implementation steps are list
|
|
|
211
281
|
});
|
|
212
282
|
});
|
|
213
283
|
});
|
|
284
|
+
|
|
285
|
+
describe('parseTaskTag', () => {
|
|
286
|
+
it('should return WEB for [WEB] tagged tasks', () => {
|
|
287
|
+
expect(parseTaskTag('[WEB]: Update root layout')).toBe('WEB');
|
|
288
|
+
expect(parseTaskTag('[web]: Update root layout')).toBe('WEB');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should return FE, BE, INT for their respective tags', () => {
|
|
292
|
+
expect(parseTaskTag('[FE]: Create component')).toBe('FE');
|
|
293
|
+
expect(parseTaskTag('[BE]: Build API')).toBe('BE');
|
|
294
|
+
expect(parseTaskTag('[INT]: Wire frontend to backend')).toBe('INT');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should return undefined for untagged tasks', () => {
|
|
298
|
+
expect(parseTaskTag('Create component')).toBeUndefined();
|
|
299
|
+
expect(parseTaskTag('Build API endpoints')).toBeUndefined();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should return undefined for unknown tags', () => {
|
|
303
|
+
expect(parseTaskTag('[UNKNOWN]: Some task')).toBeUndefined();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('tagToAppTarget', () => {
|
|
308
|
+
it('should map WEB to website', () => {
|
|
309
|
+
expect(tagToAppTarget('WEB')).toBe('website');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should map FE to frontend', () => {
|
|
313
|
+
expect(tagToAppTarget('FE')).toBe('frontend');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should map BE to backend', () => {
|
|
317
|
+
expect(tagToAppTarget('BE')).toBe('backend');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should map INT to unified', () => {
|
|
321
|
+
expect(tagToAppTarget('INT')).toBe('unified');
|
|
322
|
+
});
|
|
323
|
+
});
|