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,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for shared-packages module
|
|
3
|
+
* Verifies color scale generation and brand color passthrough
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
generateColorScale,
|
|
9
|
+
generateDesignTokensPackage,
|
|
10
|
+
} from '../../src/generators/shared-packages.js';
|
|
11
|
+
|
|
12
|
+
describe('generateColorScale', () => {
|
|
13
|
+
it('generates 10-stop color scale from valid hex', () => {
|
|
14
|
+
const scale = generateColorScale('#2563EB');
|
|
15
|
+
|
|
16
|
+
expect(Object.keys(scale)).toEqual(['50', '100', '200', '300', '400', '500', '600', '700', '800', '900']);
|
|
17
|
+
// Lightest stop should be light, darkest should be dark
|
|
18
|
+
expect(scale['50']).toMatch(/^#[0-9a-f]{6}$/);
|
|
19
|
+
expect(scale['900']).toMatch(/^#[0-9a-f]{6}$/);
|
|
20
|
+
// The stops should vary (not all the same)
|
|
21
|
+
const uniqueColors = new Set(Object.values(scale));
|
|
22
|
+
expect(uniqueColors.size).toBeGreaterThan(5);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('falls back to default sky-blue on invalid hex', () => {
|
|
26
|
+
const scale = generateColorScale('not-a-color');
|
|
27
|
+
|
|
28
|
+
// Should return the default sky-blue palette
|
|
29
|
+
expect(scale['500']).toBe('#0ea5e9');
|
|
30
|
+
expect(scale['600']).toBe('#0284c7');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('handles hex with # prefix correctly', () => {
|
|
34
|
+
const withHash = generateColorScale('#FF0000');
|
|
35
|
+
const withoutHash = generateColorScale('#FF0000');
|
|
36
|
+
|
|
37
|
+
expect(withHash).toEqual(withoutHash);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('generateDesignTokensPackage', () => {
|
|
42
|
+
it('uses brand color when provided', () => {
|
|
43
|
+
const result = generateDesignTokensPackage('test-project', {
|
|
44
|
+
primaryColor: '#2563EB',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Find the colors.ts file
|
|
48
|
+
const colorsFile = result.files.find((f) => f.path === 'src/colors.ts');
|
|
49
|
+
expect(colorsFile).toBeDefined();
|
|
50
|
+
// Should NOT contain the default sky-blue
|
|
51
|
+
expect(colorsFile!.content).not.toContain('#0ea5e9');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('uses default sky-blue when no brand color provided', () => {
|
|
55
|
+
const result = generateDesignTokensPackage('test-project');
|
|
56
|
+
|
|
57
|
+
const colorsFile = result.files.find((f) => f.path === 'src/colors.ts');
|
|
58
|
+
expect(colorsFile).toBeDefined();
|
|
59
|
+
// Should contain default sky-blue values
|
|
60
|
+
expect(colorsFile!.content).toContain('#0ea5e9');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('generates correct package structure', () => {
|
|
64
|
+
const result = generateDesignTokensPackage('my-project');
|
|
65
|
+
|
|
66
|
+
const paths = result.files.map((f) => f.path);
|
|
67
|
+
expect(paths).toContain('package.json');
|
|
68
|
+
expect(paths).toContain('tsconfig.json');
|
|
69
|
+
expect(paths).toContain('src/index.ts');
|
|
70
|
+
expect(paths).toContain('src/colors.ts');
|
|
71
|
+
expect(paths).toContain('src/typography.ts');
|
|
72
|
+
expect(paths).toContain('src/tailwind-preset.ts');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('package.json contains correct name with scope', () => {
|
|
76
|
+
const result = generateDesignTokensPackage('gateco');
|
|
77
|
+
|
|
78
|
+
const pkgFile = result.files.find((f) => f.path === 'package.json');
|
|
79
|
+
expect(pkgFile).toBeDefined();
|
|
80
|
+
const pkg = JSON.parse(pkgFile!.content);
|
|
81
|
+
expect(pkg.name).toBe('@gateco/design-tokens');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -67,7 +67,7 @@ describe('generateWebsiteHeader', () => {
|
|
|
67
67
|
it('renders text fallback when no logo', () => {
|
|
68
68
|
const header = generateWebsiteHeader('deploy-ai', contextNoLogo, mockStrategy);
|
|
69
69
|
expect(header).toContain('DeployAI');
|
|
70
|
-
expect(header).toContain('font-bold text-
|
|
70
|
+
expect(header).toContain('font-bold text-foreground');
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
it('includes navigation links from strategy', () => {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for website-config tailwind generation with brand colors
|
|
3
|
+
* Extended to verify full color token set, animations, and CSS vars
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { generateWebsiteTailwindConfig } from '../../src/generators/templates/website-config.js';
|
|
8
|
+
|
|
9
|
+
describe('generateWebsiteTailwindConfig', () => {
|
|
10
|
+
it('generates config with brand primary color', () => {
|
|
11
|
+
const config = generateWebsiteTailwindConfig({
|
|
12
|
+
primaryColor: '#2563EB',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Should contain generated color scale, NOT default sky-blue
|
|
16
|
+
expect(config).not.toContain('#0ea5e9');
|
|
17
|
+
expect(config).not.toContain('#0284c7');
|
|
18
|
+
// Should be valid TypeScript with Config type
|
|
19
|
+
expect(config).toContain("import type { Config } from 'tailwindcss'");
|
|
20
|
+
expect(config).toContain('primary:');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('includes workspace design-tokens preset import', () => {
|
|
24
|
+
const config = generateWebsiteTailwindConfig({
|
|
25
|
+
workspaceMode: true,
|
|
26
|
+
projectName: 'gateco',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(config).toContain("import designPreset from '@gateco/design-tokens/tailwind'");
|
|
30
|
+
expect(config).toContain('presets: [designPreset]');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('uses default sky-blue when no options provided', () => {
|
|
34
|
+
const config = generateWebsiteTailwindConfig();
|
|
35
|
+
|
|
36
|
+
// Should contain default sky-blue palette
|
|
37
|
+
expect(config).toContain('#0ea5e9');
|
|
38
|
+
expect(config).toContain('#0284c7');
|
|
39
|
+
// Should NOT have preset import
|
|
40
|
+
expect(config).not.toContain('designPreset');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('includes full shadcn-compatible color token set', () => {
|
|
44
|
+
const config = generateWebsiteTailwindConfig();
|
|
45
|
+
|
|
46
|
+
// Background/foreground
|
|
47
|
+
expect(config).toContain("background: 'hsl(var(--background))'");
|
|
48
|
+
expect(config).toContain("foreground: 'hsl(var(--foreground))'");
|
|
49
|
+
|
|
50
|
+
// Muted with sub-tokens
|
|
51
|
+
expect(config).toContain("muted:");
|
|
52
|
+
expect(config).toContain("hsl(var(--muted))");
|
|
53
|
+
expect(config).toContain("hsl(var(--muted-foreground))");
|
|
54
|
+
|
|
55
|
+
// Accent
|
|
56
|
+
expect(config).toContain("accent:");
|
|
57
|
+
expect(config).toContain("hsl(var(--accent))");
|
|
58
|
+
|
|
59
|
+
// Card
|
|
60
|
+
expect(config).toContain("card:");
|
|
61
|
+
expect(config).toContain("hsl(var(--card))");
|
|
62
|
+
|
|
63
|
+
// Border and ring
|
|
64
|
+
expect(config).toContain("border: 'hsl(var(--border))'");
|
|
65
|
+
expect(config).toContain("ring: 'hsl(var(--ring))'");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('includes animation utilities', () => {
|
|
69
|
+
const config = generateWebsiteTailwindConfig();
|
|
70
|
+
|
|
71
|
+
expect(config).toContain('keyframes:');
|
|
72
|
+
expect(config).toContain('fadeIn:');
|
|
73
|
+
expect(config).toContain('slideUp:');
|
|
74
|
+
expect(config).toContain('animation:');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('includes borderColor and borderRadius extensions', () => {
|
|
78
|
+
const config = generateWebsiteTailwindConfig();
|
|
79
|
+
|
|
80
|
+
expect(config).toContain('borderColor:');
|
|
81
|
+
expect(config).toContain('borderRadius:');
|
|
82
|
+
expect(config).toContain('var(--radius)');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for post-generation website content scanner
|
|
3
|
+
* Verifies detection of placeholder fingerprints in generated files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import { scanGeneratedContent } from '../../src/generators/website-content-scanner.js';
|
|
11
|
+
|
|
12
|
+
let tmpDir: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'scanner-test-'));
|
|
16
|
+
await fs.mkdir(path.join(tmpDir, 'src', 'app'), { recursive: true });
|
|
17
|
+
await fs.mkdir(path.join(tmpDir, 'src', 'components'), { recursive: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('scanGeneratedContent', () => {
|
|
25
|
+
it('produces no issues for clean files', async () => {
|
|
26
|
+
await fs.writeFile(
|
|
27
|
+
path.join(tmpDir, 'src', 'app', 'page.tsx'),
|
|
28
|
+
`export default function Home() {
|
|
29
|
+
return <main><h1>Welcome to Gateco</h1></main>;
|
|
30
|
+
}
|
|
31
|
+
`,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const result = await scanGeneratedContent(tmpDir);
|
|
35
|
+
|
|
36
|
+
expect(result.issues).toHaveLength(0);
|
|
37
|
+
expect(result.filesScanned).toBe(1);
|
|
38
|
+
expect(result.score).toBe(100);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('flags TODO block comments as errors', async () => {
|
|
42
|
+
await fs.writeFile(
|
|
43
|
+
path.join(tmpDir, 'src', 'app', 'page.tsx'),
|
|
44
|
+
`export default function Home() {
|
|
45
|
+
return <main>/* TODO: Replace with real content */</main>;
|
|
46
|
+
}
|
|
47
|
+
`,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const result = await scanGeneratedContent(tmpDir);
|
|
51
|
+
|
|
52
|
+
expect(result.issues.length).toBeGreaterThan(0);
|
|
53
|
+
const todoIssue = result.issues.find((i) => /TODO/i.test(i.message));
|
|
54
|
+
expect(todoIssue).toBeDefined();
|
|
55
|
+
expect(todoIssue!.severity).toBe('error');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('flags TODO line comments as errors', async () => {
|
|
59
|
+
await fs.writeFile(
|
|
60
|
+
path.join(tmpDir, 'src', 'components', 'Header.tsx'),
|
|
61
|
+
`// TODO: add real navigation
|
|
62
|
+
export function Header() { return <header />; }
|
|
63
|
+
`,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const result = await scanGeneratedContent(tmpDir);
|
|
67
|
+
|
|
68
|
+
expect(result.issues.some((i) => /TODO/i.test(i.message))).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('detects default pricing pattern in file content', async () => {
|
|
72
|
+
await fs.writeFile(
|
|
73
|
+
path.join(tmpDir, 'src', 'app', 'page.tsx'),
|
|
74
|
+
`const tiers = [
|
|
75
|
+
{ name: 'Starter', price: '$0/mo', features: ['1 user'] },
|
|
76
|
+
{ name: 'Pro', price: '$29/mo', features: ['10 users'] },
|
|
77
|
+
{ name: 'Enterprise', price: 'Custom', features: ['Unlimited'] },
|
|
78
|
+
];
|
|
79
|
+
export default function Page() { return <div />; }
|
|
80
|
+
`,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const result = await scanGeneratedContent(tmpDir);
|
|
84
|
+
|
|
85
|
+
expect(result.issues.some((i) => /pricing/i.test(i.message))).toBe(true);
|
|
86
|
+
expect(result.issues.some((i) => /\$29/i.test(i.message))).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('detects default tagline text', async () => {
|
|
90
|
+
await fs.writeFile(
|
|
91
|
+
path.join(tmpDir, 'src', 'components', 'Footer.tsx'),
|
|
92
|
+
`export function Footer() { return <p>Build something amazing</p>; }
|
|
93
|
+
`,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const result = await scanGeneratedContent(tmpDir);
|
|
97
|
+
|
|
98
|
+
expect(result.issues.some((i) => /tagline/i.test(i.message))).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('detects generic description text', async () => {
|
|
102
|
+
await fs.writeFile(
|
|
103
|
+
path.join(tmpDir, 'src', 'app', 'page.tsx'),
|
|
104
|
+
`export default function Page() { return <p>Your modern web application</p>; }
|
|
105
|
+
`,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const result = await scanGeneratedContent(tmpDir);
|
|
109
|
+
|
|
110
|
+
expect(result.issues.some((i) => /generic description/i.test(i.message))).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('detects default How It Works steps', async () => {
|
|
114
|
+
await fs.writeFile(
|
|
115
|
+
path.join(tmpDir, 'src', 'app', 'page.tsx'),
|
|
116
|
+
`const steps = [
|
|
117
|
+
{ title: 'Sign Up', desc: 'Create account' },
|
|
118
|
+
{ title: 'Configure', desc: 'Set preferences' },
|
|
119
|
+
{ title: 'Deploy', desc: 'Go live' },
|
|
120
|
+
];
|
|
121
|
+
export default function Page() { return <div />; }
|
|
122
|
+
`,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const result = await scanGeneratedContent(tmpDir);
|
|
126
|
+
|
|
127
|
+
expect(result.issues.some((i) => /How It Works/i.test(i.message))).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('returns score of 100 when no issues found', async () => {
|
|
131
|
+
await fs.writeFile(
|
|
132
|
+
path.join(tmpDir, 'src', 'app', 'page.tsx'),
|
|
133
|
+
`export default function Page() { return <h1>Gateco</h1>; }
|
|
134
|
+
`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const result = await scanGeneratedContent(tmpDir);
|
|
138
|
+
|
|
139
|
+
expect(result.score).toBe(100);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('decreases score with multiple issues', async () => {
|
|
143
|
+
await fs.writeFile(
|
|
144
|
+
path.join(tmpDir, 'src', 'app', 'page.tsx'),
|
|
145
|
+
`// TODO: fix this
|
|
146
|
+
export default function Page() {
|
|
147
|
+
return <div>
|
|
148
|
+
<p>Build something amazing</p>
|
|
149
|
+
<p>Your modern web application costs $29/mo</p>
|
|
150
|
+
</div>;
|
|
151
|
+
}
|
|
152
|
+
`,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const result = await scanGeneratedContent(tmpDir);
|
|
156
|
+
|
|
157
|
+
expect(result.score).toBeLessThan(80);
|
|
158
|
+
expect(result.issues.length).toBeGreaterThanOrEqual(3);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('handles empty src directory gracefully', async () => {
|
|
162
|
+
const result = await scanGeneratedContent(tmpDir);
|
|
163
|
+
|
|
164
|
+
expect(result.issues).toHaveLength(0);
|
|
165
|
+
expect(result.filesScanned).toBe(0);
|
|
166
|
+
expect(result.score).toBe(100);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('skips node_modules directory', async () => {
|
|
170
|
+
await fs.mkdir(path.join(tmpDir, 'src', 'node_modules'), { recursive: true });
|
|
171
|
+
await fs.writeFile(
|
|
172
|
+
path.join(tmpDir, 'src', 'node_modules', 'bad.tsx'),
|
|
173
|
+
'// TODO: should be ignored',
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const result = await scanGeneratedContent(tmpDir);
|
|
177
|
+
|
|
178
|
+
expect(result.filesScanned).toBe(0);
|
|
179
|
+
expect(result.issues).toHaveLength(0);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
readProjectDocs,
|
|
13
13
|
findBrandAssets,
|
|
14
14
|
buildWebsiteContext,
|
|
15
|
+
validateWebsiteContextOrThrow,
|
|
16
|
+
type WebsiteContentContext,
|
|
15
17
|
} from '../../src/generators/website-context.js';
|
|
16
18
|
|
|
17
19
|
describe('discoverProjectDocs', () => {
|
|
@@ -219,4 +221,111 @@ describe('buildWebsiteContext', () => {
|
|
|
219
221
|
// Primary color is accent-primary (#2563EB), NOT bg-primary (#0F172A)
|
|
220
222
|
expect(context.brand?.primaryColor).toBe('#2563EB');
|
|
221
223
|
});
|
|
224
|
+
|
|
225
|
+
it('discovers docs from parent directory via workspace root', async () => {
|
|
226
|
+
// Simulate: tmpDir has docs, tmpDir/project/.popeye/ is the project dir
|
|
227
|
+
const projectDir = path.join(tmpDir, 'project');
|
|
228
|
+
await fs.mkdir(path.join(projectDir, '.popeye'), { recursive: true });
|
|
229
|
+
await fs.writeFile(
|
|
230
|
+
path.join(tmpDir, 'product-spec.md'),
|
|
231
|
+
'# MyProduct — A great product\n## Features\n- Feature 1'
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const context = await buildWebsiteContext(projectDir, 'project');
|
|
235
|
+
|
|
236
|
+
// Should find docs from parent directory
|
|
237
|
+
expect(context.rawDocs).toContain('product-spec.md');
|
|
238
|
+
expect(context.productName).toBe('MyProduct');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('finds brand assets from parent directory', async () => {
|
|
242
|
+
const projectDir = path.join(tmpDir, 'project');
|
|
243
|
+
await fs.mkdir(projectDir, { recursive: true });
|
|
244
|
+
await fs.writeFile(path.join(tmpDir, 'company-logo.png'), 'fake-png');
|
|
245
|
+
|
|
246
|
+
const result = await findBrandAssets(projectDir);
|
|
247
|
+
|
|
248
|
+
// findBrandAssets now scans parent dirs via workspace root
|
|
249
|
+
// The logo should be found in tmpDir
|
|
250
|
+
expect(result.logoPath).toBeDefined();
|
|
251
|
+
expect(result.logoPath).toContain('company-logo.png');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('enforces per-file cap to prevent single large doc from consuming budget', async () => {
|
|
255
|
+
await fs.writeFile(path.join(tmpDir, 'huge-spec.md'), 'x'.repeat(20000));
|
|
256
|
+
await fs.writeFile(path.join(tmpDir, 'color-scheme.md'), '# Colors\nPrimary: #FF0000');
|
|
257
|
+
|
|
258
|
+
const docs = await discoverProjectDocs(tmpDir);
|
|
259
|
+
const content = await readProjectDocs(docs);
|
|
260
|
+
|
|
261
|
+
// Color-scheme should be present (it's prioritized and small)
|
|
262
|
+
expect(content).toContain('color-scheme.md');
|
|
263
|
+
// The huge spec should be capped at 8000 chars
|
|
264
|
+
const specSection = content.split('--- huge-spec.md ---')[1];
|
|
265
|
+
if (specSection) {
|
|
266
|
+
// Per-file cap is 8000 + "..." suffix
|
|
267
|
+
expect(specSection.length).toBeLessThan(8100);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('validateWebsiteContextOrThrow', () => {
|
|
273
|
+
const baseContext: WebsiteContentContext = {
|
|
274
|
+
productName: 'Gateco',
|
|
275
|
+
features: [{ title: 'Auth', description: 'Access control' }],
|
|
276
|
+
rawDocs: 'x'.repeat(200),
|
|
277
|
+
strategy: {
|
|
278
|
+
icp: { primaryPersona: 'devs', painPoints: [], goals: [], objections: [] },
|
|
279
|
+
positioning: { category: 'Security', differentiators: [], valueProposition: 'Secure AI', proofPoints: [] },
|
|
280
|
+
messaging: { headline: 'h', subheadline: 's', elevatorPitch: 'e', longDescription: 'l' },
|
|
281
|
+
seoStrategy: { primaryKeywords: [], secondaryKeywords: [], longTailKeywords: [], titleTemplates: {}, metaDescriptions: {} },
|
|
282
|
+
siteArchitecture: { pages: [], navigation: [], footerSections: [] },
|
|
283
|
+
conversionStrategy: { primaryCta: { text: 'Go', href: '/' }, secondaryCta: { text: 'More', href: '/' }, trustSignals: [], socialProof: [], leadCapture: 'none' },
|
|
284
|
+
competitiveContext: { category: 'sec', competitors: [], differentiators: [] },
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
it('passes with valid context', () => {
|
|
289
|
+
const result = validateWebsiteContextOrThrow(baseContext, 'gateco');
|
|
290
|
+
expect(result.passed).toBe(true);
|
|
291
|
+
expect(result.issues).toEqual([]);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('fails when strategy is missing', () => {
|
|
295
|
+
const ctx: WebsiteContentContext = { ...baseContext, strategy: undefined };
|
|
296
|
+
expect(() => validateWebsiteContextOrThrow(ctx, 'gateco')).toThrow('strategy missing');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('fails when features are empty', () => {
|
|
300
|
+
const ctx: WebsiteContentContext = { ...baseContext, features: [] };
|
|
301
|
+
expect(() => validateWebsiteContextOrThrow(ctx, 'gateco')).toThrow('features');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('fails when product name looks like a directory', () => {
|
|
305
|
+
const ctx: WebsiteContentContext = { ...baseContext, productName: 'my-cool-project' };
|
|
306
|
+
expect(() => validateWebsiteContextOrThrow(ctx, 'x')).toThrow('directory name');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('fails when no docs found', () => {
|
|
310
|
+
const ctx: WebsiteContentContext = { ...baseContext, rawDocs: '' };
|
|
311
|
+
expect(() => validateWebsiteContextOrThrow(ctx, 'gateco')).toThrow('documentation');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('fails when brand/color docs exist but no color extracted', () => {
|
|
315
|
+
const ctx: WebsiteContentContext = {
|
|
316
|
+
...baseContext,
|
|
317
|
+
rawDocs: 'x'.repeat(200) + 'color brand guide',
|
|
318
|
+
brand: undefined,
|
|
319
|
+
};
|
|
320
|
+
expect(() => validateWebsiteContextOrThrow(ctx, 'gateco')).toThrow('primary color');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('fails when logo found but output path not resolved', () => {
|
|
324
|
+
const ctx: WebsiteContentContext = {
|
|
325
|
+
...baseContext,
|
|
326
|
+
brand: { logoPath: '/some/logo.png' },
|
|
327
|
+
brandAssets: undefined,
|
|
328
|
+
};
|
|
329
|
+
expect(() => validateWebsiteContextOrThrow(ctx, 'gateco')).toThrow('output path not resolved');
|
|
330
|
+
});
|
|
222
331
|
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for website-debug module
|
|
3
|
+
* Verifies trace includes sections + validation info
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { formatDebugTrace, type WebsiteDebugTrace } from '../../src/generators/website-debug.js';
|
|
8
|
+
|
|
9
|
+
function makeTrace(overrides?: Partial<WebsiteDebugTrace>): WebsiteDebugTrace {
|
|
10
|
+
return {
|
|
11
|
+
workspaceRoot: '/tmp/project',
|
|
12
|
+
docsFound: [{ path: '/tmp/project/spec.md', size: 1000 }],
|
|
13
|
+
brandAssets: { logoPath: '/tmp/logo.png', logoOutputPath: 'public/brand/logo.png' },
|
|
14
|
+
productName: { value: 'Gateco', source: 'docs' },
|
|
15
|
+
primaryColor: { value: '#2563EB', source: 'brand-docs' },
|
|
16
|
+
strategyStatus: 'success',
|
|
17
|
+
templateValues: { headline: 'Secure AI', features: 3, pricingTiers: 3 },
|
|
18
|
+
sectionsRendered: [
|
|
19
|
+
{ name: 'Hero', dataSource: 'strategy', itemCount: 1 },
|
|
20
|
+
{ name: 'PainPoints', dataSource: 'strategy', itemCount: 3 },
|
|
21
|
+
{ name: 'Features', dataSource: 'docs', itemCount: 3 },
|
|
22
|
+
{ name: 'FAQ', dataSource: 'skipped', itemCount: 0 },
|
|
23
|
+
],
|
|
24
|
+
validationPassed: true,
|
|
25
|
+
validationIssues: [],
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('formatDebugTrace', () => {
|
|
31
|
+
it('formats basic trace fields', () => {
|
|
32
|
+
const output = formatDebugTrace(makeTrace());
|
|
33
|
+
expect(output).toContain('WEBSITE GENERATION DEBUG TRACE');
|
|
34
|
+
expect(output).toContain('Gateco');
|
|
35
|
+
expect(output).toContain('#2563EB');
|
|
36
|
+
expect(output).toContain('success');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('includes sections rendered with data sources', () => {
|
|
40
|
+
const output = formatDebugTrace(makeTrace());
|
|
41
|
+
expect(output).toContain('Sections Rendered (4)');
|
|
42
|
+
expect(output).toContain('Hero: strategy (1 items)');
|
|
43
|
+
expect(output).toContain('PainPoints: strategy (3 items)');
|
|
44
|
+
expect(output).toContain('Features: docs (3 items)');
|
|
45
|
+
expect(output).toContain('FAQ: skipped (0 items)');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('shows validation passed', () => {
|
|
49
|
+
const output = formatDebugTrace(makeTrace());
|
|
50
|
+
expect(output).toContain('Validation: PASSED');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('shows validation failed with issues', () => {
|
|
54
|
+
const output = formatDebugTrace(makeTrace({
|
|
55
|
+
validationPassed: false,
|
|
56
|
+
validationIssues: ['Strategy missing', 'No features found'],
|
|
57
|
+
}));
|
|
58
|
+
expect(output).toContain('Validation: FAILED');
|
|
59
|
+
expect(output).toContain('Strategy missing');
|
|
60
|
+
expect(output).toContain('No features found');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('handles empty sections list', () => {
|
|
64
|
+
const output = formatDebugTrace(makeTrace({ sectionsRendered: [] }));
|
|
65
|
+
expect(output).toContain('Sections Rendered (0)');
|
|
66
|
+
expect(output).toContain('(none)');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('shows strategy error when present', () => {
|
|
70
|
+
const output = formatDebugTrace(makeTrace({
|
|
71
|
+
strategyStatus: 'failed',
|
|
72
|
+
strategyError: 'Rate limit exceeded',
|
|
73
|
+
}));
|
|
74
|
+
expect(output).toContain('Strategy: failed');
|
|
75
|
+
expect(output).toContain('Error: Rate limit exceeded');
|
|
76
|
+
});
|
|
77
|
+
});
|