popeye-cli 1.5.0 → 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 +50 -8
- 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 +90 -7
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts +4 -1
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +36 -316
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +18 -3
- package/dist/generators/doc-parser.d.ts.map +1 -1
- package/dist/generators/doc-parser.js +81 -10
- package/dist/generators/doc-parser.js.map +1 -1
- 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 +4 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +4 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/generators/templates/website-components.d.ts.map +1 -1
- package/dist/generators/templates/website-components.js +36 -11
- package/dist/generators/templates/website-components.js.map +1 -1
- package/dist/generators/templates/website-config.d.ts +15 -1
- package/dist/generators/templates/website-config.d.ts.map +1 -1
- package/dist/generators/templates/website-config.js +155 -13
- package/dist/generators/templates/website-config.js.map +1 -1
- 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.d.ts +10 -50
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +12 -788
- 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 +38 -2
- package/dist/generators/website-context.d.ts.map +1 -1
- package/dist/generators/website-context.js +179 -19
- package/dist/generators/website-context.js.map +1 -1
- 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 +2 -0
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +66 -4
- 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 +8 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +10 -0
- package/dist/state/index.js.map +1 -1
- package/dist/types/workflow.d.ts +6 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +2 -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/index.d.ts +1 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +12 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/overview.d.ts.map +1 -1
- package/dist/workflow/overview.js +4 -0
- package/dist/workflow/overview.js.map +1 -1
- package/dist/workflow/plan-mode.d.ts +4 -3
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +69 -5
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts +9 -0
- package/dist/workflow/website-strategy.d.ts.map +1 -1
- package/dist/workflow/website-strategy.js +73 -1
- package/dist/workflow/website-strategy.js.map +1 -1
- package/dist/workflow/website-updater.d.ts.map +1 -1
- package/dist/workflow/website-updater.js +15 -4
- package/dist/workflow/website-updater.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/create.ts +58 -4
- package/src/cli/interactive.ts +96 -7
- package/src/generators/all.ts +44 -332
- package/src/generators/doc-parser.ts +87 -10
- package/src/generators/frontend-design-analyzer.ts +261 -0
- package/src/generators/shared-packages.ts +500 -0
- package/src/generators/templates/index.ts +4 -0
- package/src/generators/templates/website-components.ts +36 -11
- package/src/generators/templates/website-config.ts +166 -13
- 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.ts +38 -851
- package/src/generators/website-content-scanner.ts +208 -0
- package/src/generators/website-context.ts +248 -20
- package/src/generators/website-debug.ts +130 -0
- package/src/generators/website.ts +71 -3
- package/src/generators/workspace-root.ts +113 -0
- package/src/state/index.ts +14 -0
- package/src/types/workflow.ts +6 -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/index.ts +12 -0
- package/src/workflow/overview.ts +6 -0
- package/src/workflow/plan-mode.ts +81 -7
- package/src/workflow/website-strategy.ts +75 -1
- package/src/workflow/website-updater.ts +17 -6
- 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 +1 -1
- 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 +109 -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/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/plan-mode.test.ts +111 -1
- package/tests/workflow/website-strategy.test.ts +55 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for upgrade handler content context builder
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
6
|
+
import { promises as fs } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import { buildUpgradeContentContext } from '../../src/upgrade/handlers.js';
|
|
10
|
+
|
|
11
|
+
// Mock external dependencies to isolate unit behavior
|
|
12
|
+
vi.mock('../../src/generators/website-context.js', () => ({
|
|
13
|
+
buildWebsiteContext: vi.fn(),
|
|
14
|
+
resolveBrandAssets: vi.fn(),
|
|
15
|
+
validateWebsiteContext: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock('../../src/generators/workspace-root.js', () => ({
|
|
19
|
+
resolveWorkspaceRoot: vi.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock('../../src/workflow/website-strategy.js', () => ({
|
|
23
|
+
loadWebsiteStrategy: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock('../../src/state/persistence.js', () => ({
|
|
27
|
+
loadState: vi.fn(),
|
|
28
|
+
saveState: vi.fn(),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
import { buildWebsiteContext, resolveBrandAssets, validateWebsiteContext } from '../../src/generators/website-context.js';
|
|
32
|
+
import { resolveWorkspaceRoot } from '../../src/generators/workspace-root.js';
|
|
33
|
+
import { loadWebsiteStrategy } from '../../src/workflow/website-strategy.js';
|
|
34
|
+
import { loadState } from '../../src/state/persistence.js';
|
|
35
|
+
|
|
36
|
+
const mockBuildWebsiteContext = vi.mocked(buildWebsiteContext);
|
|
37
|
+
const mockResolveBrandAssets = vi.mocked(resolveBrandAssets);
|
|
38
|
+
const mockValidateWebsiteContext = vi.mocked(validateWebsiteContext);
|
|
39
|
+
const mockResolveWorkspaceRoot = vi.mocked(resolveWorkspaceRoot);
|
|
40
|
+
const mockLoadWebsiteStrategy = vi.mocked(loadWebsiteStrategy);
|
|
41
|
+
const mockLoadState = vi.mocked(loadState);
|
|
42
|
+
|
|
43
|
+
let tmpDir: string;
|
|
44
|
+
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-handlers-test-'));
|
|
47
|
+
vi.clearAllMocks();
|
|
48
|
+
|
|
49
|
+
// Default mock returns
|
|
50
|
+
mockBuildWebsiteContext.mockResolvedValue({
|
|
51
|
+
productName: 'TestProject',
|
|
52
|
+
features: [{ title: 'Feature 1', description: 'A great feature' }],
|
|
53
|
+
rawDocs: '# Test docs',
|
|
54
|
+
});
|
|
55
|
+
mockResolveBrandAssets.mockResolvedValue({
|
|
56
|
+
logoSource: null,
|
|
57
|
+
faviconSource: null,
|
|
58
|
+
targets: [],
|
|
59
|
+
});
|
|
60
|
+
mockResolveWorkspaceRoot.mockResolvedValue(tmpDir);
|
|
61
|
+
mockLoadWebsiteStrategy.mockResolvedValue(null);
|
|
62
|
+
mockLoadState.mockResolvedValue(null);
|
|
63
|
+
mockValidateWebsiteContext.mockReturnValue({
|
|
64
|
+
passed: true,
|
|
65
|
+
issues: [],
|
|
66
|
+
warnings: [],
|
|
67
|
+
contentScore: 100,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(async () => {
|
|
72
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('buildUpgradeContentContext', () => {
|
|
76
|
+
it('should build context from user docs and return features', async () => {
|
|
77
|
+
const { context, warning } = await buildUpgradeContentContext(tmpDir, 'TestProject');
|
|
78
|
+
|
|
79
|
+
expect(warning).toBeUndefined();
|
|
80
|
+
expect(context).toBeDefined();
|
|
81
|
+
expect(context!.productName).toBe('TestProject');
|
|
82
|
+
expect(context!.features).toHaveLength(1);
|
|
83
|
+
expect(context!.features[0].title).toBe('Feature 1');
|
|
84
|
+
|
|
85
|
+
// Verify buildWebsiteContext was called with correct args
|
|
86
|
+
expect(mockBuildWebsiteContext).toHaveBeenCalledWith(tmpDir, 'TestProject');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should apply brand context from state when available', async () => {
|
|
90
|
+
mockLoadState.mockResolvedValue({
|
|
91
|
+
name: 'TestProject',
|
|
92
|
+
language: 'all',
|
|
93
|
+
brandContext: {
|
|
94
|
+
primaryColor: '#2563EB',
|
|
95
|
+
logoPath: '/path/to/logo.png',
|
|
96
|
+
},
|
|
97
|
+
} as any);
|
|
98
|
+
|
|
99
|
+
const { context } = await buildUpgradeContentContext(tmpDir, 'TestProject');
|
|
100
|
+
|
|
101
|
+
expect(context!.brand).toBeDefined();
|
|
102
|
+
expect(context!.brand!.primaryColor).toBe('#2563EB');
|
|
103
|
+
expect(context!.brand!.logoPath).toBe('/path/to/logo.png');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should load website strategy when available', async () => {
|
|
107
|
+
const mockStrategy = {
|
|
108
|
+
icp: { title: 'Developer', painPoints: ['slow builds'] },
|
|
109
|
+
messaging: { headline: 'Build faster', subheadline: 'Ship more' },
|
|
110
|
+
};
|
|
111
|
+
mockLoadWebsiteStrategy.mockResolvedValue({
|
|
112
|
+
strategy: mockStrategy as any,
|
|
113
|
+
metadata: { inputHash: 'abc', generatedAt: '2024-01-01', version: '1.0' },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const { context } = await buildUpgradeContentContext(tmpDir, 'TestProject');
|
|
117
|
+
|
|
118
|
+
expect(context!.strategy).toBeDefined();
|
|
119
|
+
expect(context!.strategy).toBe(mockStrategy);
|
|
120
|
+
expect(mockLoadWebsiteStrategy).toHaveBeenCalledWith(tmpDir);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should resolve brand assets using workspace root', async () => {
|
|
124
|
+
const wsRoot = '/resolved/workspace/root';
|
|
125
|
+
mockResolveWorkspaceRoot.mockResolvedValue(wsRoot);
|
|
126
|
+
|
|
127
|
+
const { context } = await buildUpgradeContentContext(tmpDir, 'TestProject');
|
|
128
|
+
|
|
129
|
+
expect(context).toBeDefined();
|
|
130
|
+
expect(mockResolveWorkspaceRoot).toHaveBeenCalledWith(tmpDir);
|
|
131
|
+
expect(mockResolveBrandAssets).toHaveBeenCalledWith(wsRoot, context!.brand);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should return warning on error without crashing', async () => {
|
|
135
|
+
mockBuildWebsiteContext.mockRejectedValue(new Error('Docs directory not found'));
|
|
136
|
+
|
|
137
|
+
const { context, warning } = await buildUpgradeContentContext(tmpDir, 'TestProject');
|
|
138
|
+
|
|
139
|
+
expect(context).toBeUndefined();
|
|
140
|
+
expect(warning).toBe('Docs directory not found');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should handle non-Error throws gracefully', async () => {
|
|
144
|
+
mockBuildWebsiteContext.mockRejectedValue('unexpected string error');
|
|
145
|
+
|
|
146
|
+
const { context, warning } = await buildUpgradeContentContext(tmpDir, 'TestProject');
|
|
147
|
+
|
|
148
|
+
expect(context).toBeUndefined();
|
|
149
|
+
expect(warning).toBe('Unknown error building website context');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should work when no state or strategy exists', async () => {
|
|
153
|
+
// Default mocks already return null for state and strategy
|
|
154
|
+
const { context, warning } = await buildUpgradeContentContext(tmpDir, 'TestProject');
|
|
155
|
+
|
|
156
|
+
expect(warning).toBeUndefined();
|
|
157
|
+
expect(context).toBeDefined();
|
|
158
|
+
expect(context!.strategy).toBeUndefined();
|
|
159
|
+
// Brand should still be whatever buildWebsiteContext returns
|
|
160
|
+
expect(context!.productName).toBe('TestProject');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for auto-fix-bundler: CSS/PostCSS/Tailwind/webpack error parsing and config discovery
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { promises as fs } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import {
|
|
10
|
+
parseBundlerErrors,
|
|
11
|
+
findRelatedConfigs,
|
|
12
|
+
parseMultiFileResponse,
|
|
13
|
+
} from '../../src/workflow/auto-fix-bundler.js';
|
|
14
|
+
|
|
15
|
+
let tempDir: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-bundler-'));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('parseBundlerErrors', () => {
|
|
26
|
+
it('should parse Tailwind CSS "class does not exist" errors', () => {
|
|
27
|
+
const output = `./src/app/globals.css:1:1
|
|
28
|
+
Syntax error: /Users/test/project/apps/website/src/app/globals.css The \`bg-background\` class does not exist. If \`bg-background\` is a custom class, make sure it is defined within a \`@layer\` directive.
|
|
29
|
+
|
|
30
|
+
> 1 | @tailwind base;
|
|
31
|
+
| ^
|
|
32
|
+
2 | @tailwind components;
|
|
33
|
+
3 | @tailwind utilities;`;
|
|
34
|
+
|
|
35
|
+
const errors = parseBundlerErrors(output);
|
|
36
|
+
|
|
37
|
+
expect(errors.length).toBeGreaterThanOrEqual(1);
|
|
38
|
+
// Should find either the css pattern or the file:line:col pattern
|
|
39
|
+
const cssError = errors.find(e => e.type === 'css' || e.type === 'syntax');
|
|
40
|
+
expect(cssError).toBeDefined();
|
|
41
|
+
expect(cssError!.message).toContain('bg-background');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should parse CSS syntax error with absolute path', () => {
|
|
45
|
+
const output = `Syntax error: /Users/test/apps/website/src/globals.css The \`text-foreground\` class does not exist. If \`text-foreground\` is a custom class, make sure it is defined within a \`@layer\` directive.
|
|
46
|
+
|
|
47
|
+
Some other lines here.`;
|
|
48
|
+
|
|
49
|
+
const errors = parseBundlerErrors(output);
|
|
50
|
+
|
|
51
|
+
expect(errors.length).toBe(1);
|
|
52
|
+
expect(errors[0].type).toBe('css');
|
|
53
|
+
expect(errors[0].file).toContain('globals.css');
|
|
54
|
+
expect(errors[0].message).toContain('text-foreground');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should parse module not found errors', () => {
|
|
58
|
+
const output = `Module not found: Can't resolve '@acme/design-tokens/tailwind' in '/Users/test/apps/website'
|
|
59
|
+
|
|
60
|
+
https://nextjs.org/docs/messages/module-not-found
|
|
61
|
+
|
|
62
|
+
Import trace for requested module:
|
|
63
|
+
./src/app/globals.css`;
|
|
64
|
+
|
|
65
|
+
const errors = parseBundlerErrors(output);
|
|
66
|
+
|
|
67
|
+
expect(errors.length).toBeGreaterThanOrEqual(1);
|
|
68
|
+
const moduleError = errors.find(e => e.type === 'module-not-found');
|
|
69
|
+
expect(moduleError).toBeDefined();
|
|
70
|
+
expect(moduleError!.message).toContain('@acme/design-tokens/tailwind');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should parse file:line:col reference for non-TS files', () => {
|
|
74
|
+
const output = `./src/app/globals.css:1:1
|
|
75
|
+
Syntax error: Something went wrong with the CSS processing.
|
|
76
|
+
|
|
77
|
+
> 1 | @tailwind base;
|
|
78
|
+
| ^`;
|
|
79
|
+
|
|
80
|
+
const errors = parseBundlerErrors(output);
|
|
81
|
+
|
|
82
|
+
expect(errors.length).toBeGreaterThanOrEqual(1);
|
|
83
|
+
const refError = errors.find(e => e.type === 'syntax');
|
|
84
|
+
expect(refError).toBeDefined();
|
|
85
|
+
expect(refError!.file).toContain('globals.css');
|
|
86
|
+
expect(refError!.line).toBe(1);
|
|
87
|
+
expect(refError!.column).toBe(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should parse webpack build failure with import traces', () => {
|
|
91
|
+
const output = `Build failed because of webpack errors
|
|
92
|
+
|
|
93
|
+
Import trace for requested module:
|
|
94
|
+
./src/app/globals.css
|
|
95
|
+
./src/components/Layout.tsx`;
|
|
96
|
+
|
|
97
|
+
const errors = parseBundlerErrors(output);
|
|
98
|
+
|
|
99
|
+
expect(errors.length).toBeGreaterThanOrEqual(1);
|
|
100
|
+
expect(errors.some(e => e.file.includes('globals.css'))).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should strip ANSI color codes before parsing', () => {
|
|
104
|
+
const output = `\x1b[31mSyntax error: /path/to/file.css The \`bg-primary\` class does not exist.\x1b[0m`;
|
|
105
|
+
|
|
106
|
+
const errors = parseBundlerErrors(output);
|
|
107
|
+
|
|
108
|
+
expect(errors.length).toBe(1);
|
|
109
|
+
expect(errors[0].message).toContain('bg-primary');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should de-duplicate errors from the same file', () => {
|
|
113
|
+
const output = `Syntax error: /path/to/globals.css The \`bg-background\` class does not exist.
|
|
114
|
+
|
|
115
|
+
Syntax error: /path/to/globals.css The \`bg-background\` class does not exist.`;
|
|
116
|
+
|
|
117
|
+
const errors = parseBundlerErrors(output);
|
|
118
|
+
|
|
119
|
+
// Should de-duplicate by file
|
|
120
|
+
expect(errors.length).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should return empty array for TypeScript-only errors', () => {
|
|
124
|
+
const output = `src/index.ts(10,5): error TS2304: Cannot find name 'foo'
|
|
125
|
+
src/utils.ts(25,12): error TS2339: Property 'bar' does not exist`;
|
|
126
|
+
|
|
127
|
+
const errors = parseBundlerErrors(output);
|
|
128
|
+
|
|
129
|
+
expect(errors.length).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should return empty array for generic npm errors', () => {
|
|
133
|
+
const output = `npm ERR! code ELIFECYCLE
|
|
134
|
+
npm ERR! errno 1
|
|
135
|
+
npm ERR! Exit status 1`;
|
|
136
|
+
|
|
137
|
+
const errors = parseBundlerErrors(output);
|
|
138
|
+
|
|
139
|
+
expect(errors.length).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should return empty array for empty output', () => {
|
|
143
|
+
expect(parseBundlerErrors('')).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('findRelatedConfigs', () => {
|
|
148
|
+
it('should find tailwind and postcss configs in app directory', async () => {
|
|
149
|
+
const appDir = path.join(tempDir, 'apps', 'website');
|
|
150
|
+
await fs.mkdir(appDir, { recursive: true });
|
|
151
|
+
await fs.writeFile(path.join(appDir, 'tailwind.config.ts'), 'export default {}');
|
|
152
|
+
await fs.writeFile(path.join(appDir, 'postcss.config.js'), 'module.exports = {}');
|
|
153
|
+
|
|
154
|
+
const configs = await findRelatedConfigs(tempDir, 'apps/website/src/globals.css');
|
|
155
|
+
|
|
156
|
+
expect(configs.length).toBeGreaterThanOrEqual(2);
|
|
157
|
+
expect(configs.some(c => c.path.includes('tailwind.config.ts'))).toBe(true);
|
|
158
|
+
expect(configs.some(c => c.path.includes('postcss.config.js'))).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should search project root as well', async () => {
|
|
162
|
+
await fs.writeFile(path.join(tempDir, 'package.json'), '{"name": "test"}');
|
|
163
|
+
|
|
164
|
+
const configs = await findRelatedConfigs(tempDir, 'src/globals.css');
|
|
165
|
+
|
|
166
|
+
expect(configs.some(c => c.path.includes('package.json'))).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should return empty array when no configs exist', async () => {
|
|
170
|
+
const configs = await findRelatedConfigs(tempDir, 'src/globals.css');
|
|
171
|
+
|
|
172
|
+
expect(configs).toEqual([]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should cap config file content at 4000 chars', async () => {
|
|
176
|
+
const largeContent = 'x'.repeat(10000);
|
|
177
|
+
await fs.writeFile(path.join(tempDir, 'package.json'), largeContent);
|
|
178
|
+
|
|
179
|
+
const configs = await findRelatedConfigs(tempDir, 'src/globals.css');
|
|
180
|
+
|
|
181
|
+
const pkg = configs.find(c => c.path.includes('package.json'));
|
|
182
|
+
expect(pkg).toBeDefined();
|
|
183
|
+
expect(pkg!.content.length).toBeLessThanOrEqual(4000);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('parseMultiFileResponse', () => {
|
|
188
|
+
it('should parse single file response', () => {
|
|
189
|
+
const response = `FILE: /path/to/tailwind.config.ts
|
|
190
|
+
\`\`\`typescript
|
|
191
|
+
import type { Config } from 'tailwindcss';
|
|
192
|
+
export default { content: [] } satisfies Config;
|
|
193
|
+
\`\`\``;
|
|
194
|
+
|
|
195
|
+
const results = parseMultiFileResponse(response);
|
|
196
|
+
|
|
197
|
+
expect(results.length).toBe(1);
|
|
198
|
+
expect(results[0].targetPath).toBe('/path/to/tailwind.config.ts');
|
|
199
|
+
expect(results[0].content).toContain('tailwindcss');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should parse multiple file responses', () => {
|
|
203
|
+
const response = `I'll fix both files:
|
|
204
|
+
|
|
205
|
+
FILE: /path/to/tailwind.config.ts
|
|
206
|
+
\`\`\`
|
|
207
|
+
export default { colors: { background: 'hsl(var(--background))' } };
|
|
208
|
+
\`\`\`
|
|
209
|
+
|
|
210
|
+
FILE: /path/to/globals.css
|
|
211
|
+
\`\`\`
|
|
212
|
+
@tailwind base;
|
|
213
|
+
@tailwind components;
|
|
214
|
+
@tailwind utilities;
|
|
215
|
+
\`\`\``;
|
|
216
|
+
|
|
217
|
+
const results = parseMultiFileResponse(response);
|
|
218
|
+
|
|
219
|
+
expect(results.length).toBe(2);
|
|
220
|
+
expect(results[0].targetPath).toContain('tailwind.config.ts');
|
|
221
|
+
expect(results[1].targetPath).toContain('globals.css');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should skip files with very short content', () => {
|
|
225
|
+
const response = `FILE: /path/to/file.ts
|
|
226
|
+
\`\`\`
|
|
227
|
+
tiny
|
|
228
|
+
\`\`\``;
|
|
229
|
+
|
|
230
|
+
const results = parseMultiFileResponse(response);
|
|
231
|
+
|
|
232
|
+
expect(results.length).toBe(0);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should return empty array for unparseable response', () => {
|
|
236
|
+
const response = `I'm not sure how to fix this error.`;
|
|
237
|
+
|
|
238
|
+
const results = parseMultiFileResponse(response);
|
|
239
|
+
|
|
240
|
+
expect(results.length).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
loadWebsiteStrategy,
|
|
12
12
|
formatStrategyForPlanContext,
|
|
13
13
|
isStrategyStale,
|
|
14
|
+
packProductContext,
|
|
14
15
|
} from '../../src/workflow/website-strategy.js';
|
|
15
16
|
import type {
|
|
16
17
|
WebsiteStrategyDocument,
|
|
@@ -189,3 +190,57 @@ describe('isStrategyStale', () => {
|
|
|
189
190
|
expect(loaded!.strategy.messaging.headline).toBe('Ship Code 10x Faster');
|
|
190
191
|
});
|
|
191
192
|
});
|
|
193
|
+
|
|
194
|
+
describe('packProductContext', () => {
|
|
195
|
+
it('preserves high-priority docs (spec, pricing) within budget', () => {
|
|
196
|
+
const context = [
|
|
197
|
+
'--- random-notes.md ---',
|
|
198
|
+
'Some random notes about the project.',
|
|
199
|
+
'',
|
|
200
|
+
'--- product-spec.md ---',
|
|
201
|
+
'# Product Specification\nThis is the product specification.',
|
|
202
|
+
'',
|
|
203
|
+
'--- pricing.md ---',
|
|
204
|
+
'# Pricing\nFree, Pro, Enterprise tiers.',
|
|
205
|
+
'',
|
|
206
|
+
'--- color-scheme.md ---',
|
|
207
|
+
'# Colors\nPrimary: #2563EB',
|
|
208
|
+
].join('\n');
|
|
209
|
+
|
|
210
|
+
const packed = packProductContext(context, 500);
|
|
211
|
+
|
|
212
|
+
// Spec should come first (priority 1)
|
|
213
|
+
const specIndex = packed.indexOf('product-spec.md');
|
|
214
|
+
const pricingIndex = packed.indexOf('pricing.md');
|
|
215
|
+
const randomIndex = packed.indexOf('random-notes.md');
|
|
216
|
+
|
|
217
|
+
expect(specIndex).toBeGreaterThanOrEqual(0);
|
|
218
|
+
expect(pricingIndex).toBeGreaterThanOrEqual(0);
|
|
219
|
+
// Spec should appear before random notes (or random notes may be cut)
|
|
220
|
+
if (randomIndex >= 0) {
|
|
221
|
+
expect(specIndex).toBeLessThan(randomIndex);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('handles context without headers gracefully', () => {
|
|
226
|
+
const context = 'Just raw text without any headers';
|
|
227
|
+
|
|
228
|
+
const packed = packProductContext(context, 100);
|
|
229
|
+
|
|
230
|
+
expect(packed).toBe(context);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('respects budget limit', () => {
|
|
234
|
+
const largeContext = [
|
|
235
|
+
'--- spec.md ---',
|
|
236
|
+
'x'.repeat(5000),
|
|
237
|
+
'',
|
|
238
|
+
'--- pricing.md ---',
|
|
239
|
+
'y'.repeat(5000),
|
|
240
|
+
].join('\n');
|
|
241
|
+
|
|
242
|
+
const packed = packProductContext(largeContext, 1000);
|
|
243
|
+
|
|
244
|
+
expect(packed.length).toBeLessThanOrEqual(1100); // Some tolerance for headers
|
|
245
|
+
});
|
|
246
|
+
});
|