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,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for website-pricing module
|
|
3
|
+
* Verifies pricing page generation with tiers, comparison table, and FAQ
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { generateWebsitePricingPage } from '../../src/generators/templates/website-pricing.js';
|
|
8
|
+
import type { WebsiteContentContext } from '../../src/generators/website-context.js';
|
|
9
|
+
|
|
10
|
+
function makeContext(): WebsiteContentContext {
|
|
11
|
+
return {
|
|
12
|
+
productName: 'Gateco',
|
|
13
|
+
features: [],
|
|
14
|
+
rawDocs: 'docs',
|
|
15
|
+
pricing: [
|
|
16
|
+
{
|
|
17
|
+
name: 'Free',
|
|
18
|
+
price: 'Free',
|
|
19
|
+
period: '',
|
|
20
|
+
description: 'Dev & POC',
|
|
21
|
+
features: ['Basic access', 'Community support'],
|
|
22
|
+
cta: 'Get started',
|
|
23
|
+
featured: false,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'Pro',
|
|
27
|
+
price: '$99',
|
|
28
|
+
period: '/month',
|
|
29
|
+
description: 'Production workloads',
|
|
30
|
+
features: ['Basic access', 'Priority support', 'Advanced analytics'],
|
|
31
|
+
cta: 'Start free trial',
|
|
32
|
+
featured: true,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'Enterprise',
|
|
36
|
+
price: 'Custom',
|
|
37
|
+
period: '',
|
|
38
|
+
description: 'Regulated environments',
|
|
39
|
+
features: ['Basic access', 'Priority support', 'Advanced analytics', 'SLA guarantee'],
|
|
40
|
+
cta: 'Contact sales',
|
|
41
|
+
featured: false,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('generateWebsitePricingPage', () => {
|
|
48
|
+
it('generates page with tier cards', () => {
|
|
49
|
+
const code = generateWebsitePricingPage('gateco', makeContext());
|
|
50
|
+
expect(code).toContain('Free');
|
|
51
|
+
expect(code).toContain('$99');
|
|
52
|
+
expect(code).toContain('Custom');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('includes monthly/annual toggle', () => {
|
|
56
|
+
const code = generateWebsitePricingPage('gateco', makeContext());
|
|
57
|
+
expect(code).toContain('Annual');
|
|
58
|
+
expect(code).toContain('Save 20%');
|
|
59
|
+
expect(code).toContain('useState(false)');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('marks featured tier as Most Popular', () => {
|
|
63
|
+
const code = generateWebsitePricingPage('gateco', makeContext());
|
|
64
|
+
expect(code).toContain('Most Popular');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('includes feature comparison table when tiers have features', () => {
|
|
68
|
+
const code = generateWebsitePricingPage('gateco', makeContext());
|
|
69
|
+
expect(code).toContain('Compare plans');
|
|
70
|
+
expect(code).toContain('Basic access');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('includes pricing FAQ', () => {
|
|
74
|
+
const code = generateWebsitePricingPage('gateco', makeContext());
|
|
75
|
+
expect(code).toContain('Pricing FAQ');
|
|
76
|
+
expect(code).toContain('switch plans');
|
|
77
|
+
expect(code).toContain('free trial');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('includes enterprise CTA section', () => {
|
|
81
|
+
const code = generateWebsitePricingPage('gateco', makeContext());
|
|
82
|
+
expect(code).toContain('custom plan');
|
|
83
|
+
expect(code).toContain('/contact');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('renders single H1', () => {
|
|
87
|
+
const code = generateWebsitePricingPage('gateco', makeContext());
|
|
88
|
+
const h1Count = (code.match(/<h1/g) || []).length;
|
|
89
|
+
expect(h1Count).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('generates defaults when no pricing provided', () => {
|
|
93
|
+
const code = generateWebsitePricingPage('gateco');
|
|
94
|
+
expect(code).toContain('Starter');
|
|
95
|
+
expect(code).toContain('Pro');
|
|
96
|
+
expect(code).toContain('Enterprise');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for website-sections module
|
|
3
|
+
* Verifies FAQ, Stats (no fake numbers), HowItWorks, PainPoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
mapFeatureIcon,
|
|
9
|
+
isNumericMetric,
|
|
10
|
+
generatePainPointsSection,
|
|
11
|
+
generateDifferentiatorsSection,
|
|
12
|
+
generateHowItWorksSection,
|
|
13
|
+
generateStatsSection,
|
|
14
|
+
generateSocialProofSection,
|
|
15
|
+
generateFaqSection,
|
|
16
|
+
buildFaqItemsDeclaration,
|
|
17
|
+
generateFaqItemComponent,
|
|
18
|
+
generatePricingTeaserSection,
|
|
19
|
+
} from '../../src/generators/templates/website-sections.js';
|
|
20
|
+
import type { WebsiteStrategyDocument } from '../../src/types/website-strategy.js';
|
|
21
|
+
|
|
22
|
+
function makeStrategy(overrides?: Partial<WebsiteStrategyDocument>): WebsiteStrategyDocument {
|
|
23
|
+
return {
|
|
24
|
+
icp: { primaryPersona: 'devs', painPoints: ['No ACL', 'Slow queries'], goals: [], objections: ['Is it secure?'] },
|
|
25
|
+
positioning: {
|
|
26
|
+
category: 'Security',
|
|
27
|
+
differentiators: ['Zero-trust', 'DB agnostic'],
|
|
28
|
+
valueProposition: 'Secure AI access',
|
|
29
|
+
proofPoints: ['SOC2 compliant', '10K+ queries/sec', '99.9% uptime'],
|
|
30
|
+
},
|
|
31
|
+
messaging: { headline: 'h', subheadline: 's', elevatorPitch: 'e', longDescription: 'l' },
|
|
32
|
+
seoStrategy: { primaryKeywords: [], secondaryKeywords: [], longTailKeywords: [], titleTemplates: {}, metaDescriptions: {} },
|
|
33
|
+
siteArchitecture: { pages: [{ path: '/', title: 'Home', purpose: '', pageType: 'landing', sections: ['Sign Up', 'Configure', 'Deploy'], seoKeywords: [], conversionGoal: '' }], navigation: [], footerSections: [] },
|
|
34
|
+
conversionStrategy: { primaryCta: { text: 'Go', href: '/' }, secondaryCta: { text: 'More', href: '/' }, trustSignals: [], socialProof: ['Great tool!'], leadCapture: 'none' },
|
|
35
|
+
competitiveContext: { category: 'sec', competitors: [], differentiators: [] },
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('mapFeatureIcon', () => {
|
|
41
|
+
it('maps security-related features to Shield icon', () => {
|
|
42
|
+
expect(mapFeatureIcon('Access Control')).toBe('Shield');
|
|
43
|
+
expect(mapFeatureIcon('Authentication')).toBe('Shield');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('maps search features to Search icon', () => {
|
|
47
|
+
expect(mapFeatureIcon('Vector Search')).toBe('Search');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns Star for unknown features', () => {
|
|
51
|
+
expect(mapFeatureIcon('Something Unique')).toBe('Star');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('isNumericMetric', () => {
|
|
56
|
+
it('detects numeric metrics', () => {
|
|
57
|
+
expect(isNumericMetric('10K+ queries/sec')).toBe(true);
|
|
58
|
+
expect(isNumericMetric('99.9% uptime')).toBe(true);
|
|
59
|
+
expect(isNumericMetric('500+ customers')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('rejects qualitative metrics', () => {
|
|
63
|
+
expect(isNumericMetric('SOC2 compliant')).toBe(false);
|
|
64
|
+
expect(isNumericMetric('Enterprise ready')).toBe(false);
|
|
65
|
+
expect(isNumericMetric('Audit-ready')).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('generatePainPointsSection', () => {
|
|
70
|
+
it('renders pain points from strategy', () => {
|
|
71
|
+
const { jsx, info } = generatePainPointsSection(makeStrategy());
|
|
72
|
+
expect(jsx).toContain('No ACL');
|
|
73
|
+
expect(jsx).toContain('Slow queries');
|
|
74
|
+
expect(info.dataSource).toBe('strategy');
|
|
75
|
+
expect(info.itemCount).toBe(2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('skips when no pain points', () => {
|
|
79
|
+
const strategy = makeStrategy({ icp: { primaryPersona: 'devs', painPoints: [], goals: [], objections: [] } });
|
|
80
|
+
const { jsx, info } = generatePainPointsSection(strategy);
|
|
81
|
+
expect(jsx).toBe('');
|
|
82
|
+
expect(info.dataSource).toBe('skipped');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('generateDifferentiatorsSection', () => {
|
|
87
|
+
it('renders differentiators with value proposition heading', () => {
|
|
88
|
+
const { jsx, info } = generateDifferentiatorsSection(makeStrategy());
|
|
89
|
+
expect(jsx).toContain('Secure AI access');
|
|
90
|
+
expect(jsx).toContain('Zero-trust');
|
|
91
|
+
expect(info.dataSource).toBe('strategy');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('skips when no differentiators', () => {
|
|
95
|
+
const strategy = makeStrategy({
|
|
96
|
+
positioning: { category: 'x', differentiators: [], valueProposition: '', proofPoints: [] },
|
|
97
|
+
});
|
|
98
|
+
const { jsx, info } = generateDifferentiatorsSection(strategy);
|
|
99
|
+
expect(jsx).toBe('');
|
|
100
|
+
expect(info.dataSource).toBe('skipped');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('generateHowItWorksSection', () => {
|
|
105
|
+
it('renders with strategy sections', () => {
|
|
106
|
+
const { jsx, info } = generateHowItWorksSection(makeStrategy());
|
|
107
|
+
expect(jsx).toContain('How it works');
|
|
108
|
+
expect(info.dataSource).toBe('strategy');
|
|
109
|
+
expect(info.itemCount).toBe(3);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('uses defaults when no strategy', () => {
|
|
113
|
+
const { jsx, info } = generateHowItWorksSection(undefined);
|
|
114
|
+
expect(jsx).toContain('Sign Up');
|
|
115
|
+
expect(jsx).toContain('Configure');
|
|
116
|
+
expect(jsx).toContain('Deploy');
|
|
117
|
+
expect(info.dataSource).toBe('defaults');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('generateStatsSection', () => {
|
|
122
|
+
it('renders numeric metrics as stats and qualitative as badges', () => {
|
|
123
|
+
const { jsx, info } = generateStatsSection(makeStrategy());
|
|
124
|
+
|
|
125
|
+
// Numeric: rendered as big stat
|
|
126
|
+
expect(jsx).toContain('10K+ queries/sec');
|
|
127
|
+
expect(jsx).toContain('99.9% uptime');
|
|
128
|
+
|
|
129
|
+
// Qualitative: rendered as badge
|
|
130
|
+
expect(jsx).toContain('SOC2 compliant');
|
|
131
|
+
|
|
132
|
+
expect(info.dataSource).toBe('strategy');
|
|
133
|
+
expect(info.itemCount).toBe(3);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('does NOT fabricate numeric stats from qualitative data', () => {
|
|
137
|
+
const strategy = makeStrategy({
|
|
138
|
+
positioning: {
|
|
139
|
+
category: 'x',
|
|
140
|
+
differentiators: [],
|
|
141
|
+
valueProposition: '',
|
|
142
|
+
proofPoints: ['Audit-ready', 'HIPAA compliant'],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
const { jsx } = generateStatsSection(strategy);
|
|
146
|
+
|
|
147
|
+
// Should NOT contain fabricated numbers
|
|
148
|
+
expect(jsx).not.toContain('10K+');
|
|
149
|
+
expect(jsx).not.toContain('99.9%');
|
|
150
|
+
// Should contain qualitative badges
|
|
151
|
+
expect(jsx).toContain('Audit-ready');
|
|
152
|
+
expect(jsx).toContain('HIPAA compliant');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('skips when no proof points', () => {
|
|
156
|
+
const strategy = makeStrategy({
|
|
157
|
+
positioning: { category: 'x', differentiators: [], valueProposition: '', proofPoints: [] },
|
|
158
|
+
});
|
|
159
|
+
const { jsx, info } = generateStatsSection(strategy);
|
|
160
|
+
expect(jsx).toBe('');
|
|
161
|
+
expect(info.dataSource).toBe('skipped');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('generateSocialProofSection', () => {
|
|
166
|
+
it('renders social proof quotes', () => {
|
|
167
|
+
const { jsx, info } = generateSocialProofSection(makeStrategy());
|
|
168
|
+
expect(jsx).toContain('Great tool!');
|
|
169
|
+
expect(info.dataSource).toBe('strategy');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('skips when empty', () => {
|
|
173
|
+
const strategy = makeStrategy({
|
|
174
|
+
conversionStrategy: {
|
|
175
|
+
primaryCta: { text: 'Go', href: '/' },
|
|
176
|
+
secondaryCta: { text: 'More', href: '/' },
|
|
177
|
+
trustSignals: [],
|
|
178
|
+
socialProof: [],
|
|
179
|
+
leadCapture: 'none',
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
const { jsx, info } = generateSocialProofSection(strategy);
|
|
183
|
+
expect(jsx).toBe('');
|
|
184
|
+
expect(info.dataSource).toBe('skipped');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('generateFaqSection', () => {
|
|
189
|
+
it('renders FAQ from objections', () => {
|
|
190
|
+
const { jsx, info, needsClientDirective } = generateFaqSection(makeStrategy());
|
|
191
|
+
// FAQ section JSX references faqItems via runtime mapping
|
|
192
|
+
expect(jsx).toContain('FaqItem');
|
|
193
|
+
expect(jsx).toContain('faqItems.map');
|
|
194
|
+
expect(info.dataSource).toBe('strategy');
|
|
195
|
+
expect(needsClientDirective).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('builds FAQ items declaration with objection content', () => {
|
|
199
|
+
const decl = buildFaqItemsDeclaration(makeStrategy());
|
|
200
|
+
expect(decl).toContain('Is it secure?');
|
|
201
|
+
expect(decl).toContain('faqItems');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('includes keyboard-accessible accordion in FaqItem component', () => {
|
|
205
|
+
const component = generateFaqItemComponent();
|
|
206
|
+
expect(component).toContain('aria-expanded');
|
|
207
|
+
expect(component).toContain('onClick');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('skips when no objections', () => {
|
|
211
|
+
const strategy = makeStrategy({
|
|
212
|
+
icp: { primaryPersona: 'devs', painPoints: [], goals: [], objections: [] },
|
|
213
|
+
});
|
|
214
|
+
const { jsx, info } = generateFaqSection(strategy);
|
|
215
|
+
expect(jsx).toBe('');
|
|
216
|
+
expect(info.dataSource).toBe('skipped');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('generatePricingTeaserSection', () => {
|
|
221
|
+
it('renders teaser when pricing exists', () => {
|
|
222
|
+
const { jsx, info } = generatePricingTeaserSection({
|
|
223
|
+
productName: 'Test',
|
|
224
|
+
features: [],
|
|
225
|
+
rawDocs: '',
|
|
226
|
+
pricing: [
|
|
227
|
+
{ name: 'Free', price: 'Free', description: 'Basic', features: [], cta: 'Start' },
|
|
228
|
+
{ name: 'Pro', price: '$99', period: '/mo', description: 'Full', features: [], cta: 'Buy', featured: true },
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
expect(jsx).toContain('$99');
|
|
232
|
+
expect(jsx).toContain('View full pricing');
|
|
233
|
+
expect(info.dataSource).toBe('docs');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('skips when no pricing', () => {
|
|
237
|
+
const { jsx, info } = generatePricingTeaserSection({
|
|
238
|
+
productName: 'Test',
|
|
239
|
+
features: [],
|
|
240
|
+
rawDocs: '',
|
|
241
|
+
});
|
|
242
|
+
expect(jsx).toBe('');
|
|
243
|
+
expect(info.dataSource).toBe('skipped');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEO + accessibility quality gate tests
|
|
3
|
+
* Validates that generated website pages have proper SEO tags,
|
|
4
|
+
* heading hierarchy, JSON-LD, OpenGraph, and accessibility attributes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
generateWebsiteLandingPage,
|
|
10
|
+
generateWebsiteLayout,
|
|
11
|
+
generateWebsitePricingPage,
|
|
12
|
+
} from '../../src/generators/templates/website.js';
|
|
13
|
+
import {
|
|
14
|
+
generateJsonLdComponent,
|
|
15
|
+
generateEnhancedSitemap,
|
|
16
|
+
generate404Page,
|
|
17
|
+
generate500Page,
|
|
18
|
+
generateWebManifest,
|
|
19
|
+
generateMetaHelper,
|
|
20
|
+
} from '../../src/generators/templates/website-seo.js';
|
|
21
|
+
import {
|
|
22
|
+
generateWebsiteHeader,
|
|
23
|
+
generateWebsiteFooter,
|
|
24
|
+
} from '../../src/generators/templates/website-components.js';
|
|
25
|
+
import type { WebsiteContentContext } from '../../src/generators/website-context.js';
|
|
26
|
+
import type { WebsiteStrategyDocument } from '../../src/types/website-strategy.js';
|
|
27
|
+
|
|
28
|
+
const mockStrategy: WebsiteStrategyDocument = {
|
|
29
|
+
icp: {
|
|
30
|
+
primaryPersona: 'Engineering managers at mid-size companies',
|
|
31
|
+
painPoints: ['Slow deployments', 'Poor visibility'],
|
|
32
|
+
goals: ['Ship faster', 'Better monitoring'],
|
|
33
|
+
objections: ['Learning curve', 'Migration cost'],
|
|
34
|
+
},
|
|
35
|
+
positioning: {
|
|
36
|
+
category: 'Developer Tools',
|
|
37
|
+
differentiators: ['AI-powered', 'Zero config'],
|
|
38
|
+
valueProposition: 'Deploy 10x faster with AI-powered CI/CD',
|
|
39
|
+
proofPoints: ['Used by 500+ teams'],
|
|
40
|
+
},
|
|
41
|
+
messaging: {
|
|
42
|
+
headline: 'Ship Code 10x Faster',
|
|
43
|
+
subheadline: 'AI-Powered CI/CD for Modern Teams',
|
|
44
|
+
elevatorPitch: 'Deploy with confidence using AI that learns your codebase.',
|
|
45
|
+
longDescription: 'An AI-powered CI/CD platform that analyzes your codebase and optimizes build pipelines automatically.',
|
|
46
|
+
},
|
|
47
|
+
seoStrategy: {
|
|
48
|
+
primaryKeywords: ['CI/CD', 'deployment automation', 'AI DevOps'],
|
|
49
|
+
secondaryKeywords: ['continuous integration', 'deployment pipeline'],
|
|
50
|
+
longTailKeywords: ['AI-powered CI/CD platform', 'automated deployment tool'],
|
|
51
|
+
titleTemplates: { home: 'Ship Code 10x Faster', pricing: 'Plans & Pricing' },
|
|
52
|
+
metaDescriptions: { home: 'AI-powered CI/CD platform', pricing: 'Simple, transparent pricing' },
|
|
53
|
+
},
|
|
54
|
+
siteArchitecture: {
|
|
55
|
+
pages: [
|
|
56
|
+
{ path: '/', title: 'Home', purpose: 'conversion', pageType: 'landing', sections: ['hero', 'features'], seoKeywords: ['ci/cd'], conversionGoal: 'sign up' },
|
|
57
|
+
{ path: '/pricing', title: 'Pricing', purpose: 'pricing', pageType: 'pricing', sections: ['tiers'], seoKeywords: ['pricing'], conversionGoal: 'start trial' },
|
|
58
|
+
{ path: '/docs', title: 'Docs', purpose: 'education', pageType: 'docs', sections: ['getting-started'], seoKeywords: ['docs'], conversionGoal: 'adopt' },
|
|
59
|
+
],
|
|
60
|
+
navigation: [
|
|
61
|
+
{ label: 'Features', href: '/#features' },
|
|
62
|
+
{ label: 'Pricing', href: '/pricing' },
|
|
63
|
+
{ label: 'Docs', href: '/docs' },
|
|
64
|
+
],
|
|
65
|
+
footerSections: [
|
|
66
|
+
{ title: 'Product', links: [{ label: 'Features', href: '/#features' }] },
|
|
67
|
+
{ title: 'Legal', links: [{ label: 'Privacy', href: '/privacy' }] },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
conversionStrategy: {
|
|
71
|
+
primaryCta: { text: 'Start Free Trial', href: '/pricing' },
|
|
72
|
+
secondaryCta: { text: 'Read Docs', href: '/docs' },
|
|
73
|
+
trustSignals: ['SOC 2 Compliant', 'GDPR Ready'],
|
|
74
|
+
socialProof: ['Used by 500+ engineering teams worldwide'],
|
|
75
|
+
leadCapture: 'webhook',
|
|
76
|
+
},
|
|
77
|
+
competitiveContext: {
|
|
78
|
+
category: 'CI/CD',
|
|
79
|
+
competitors: ['CircleCI', 'GitHub Actions'],
|
|
80
|
+
differentiators: ['AI-powered pipeline optimization'],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const mockContext: WebsiteContentContext = {
|
|
85
|
+
productName: 'DeployAI',
|
|
86
|
+
tagline: 'Ship Code 10x Faster',
|
|
87
|
+
description: 'An AI-powered CI/CD platform.',
|
|
88
|
+
features: [
|
|
89
|
+
{ title: 'AI Optimization', description: 'Automatically optimize build pipelines' },
|
|
90
|
+
{ title: 'Zero Config', description: 'Works out of the box' },
|
|
91
|
+
{ title: 'Real-time Monitoring', description: 'Full observability' },
|
|
92
|
+
],
|
|
93
|
+
brand: { primaryColor: '#2563EB' },
|
|
94
|
+
rawDocs: '',
|
|
95
|
+
strategy: mockStrategy,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
describe('SEO Quality Gates', () => {
|
|
99
|
+
it('layout has title and description metadata', () => {
|
|
100
|
+
const layout = generateWebsiteLayout('deploy-ai', mockContext);
|
|
101
|
+
expect(layout).toContain('title:');
|
|
102
|
+
expect(layout).toContain('description:');
|
|
103
|
+
expect(layout).toContain('DeployAI');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('landing page has exactly one H1 tag', () => {
|
|
107
|
+
const page = generateWebsiteLandingPage('deploy-ai', mockContext);
|
|
108
|
+
const h1Matches = page.match(/<h1[\s>]/g);
|
|
109
|
+
expect(h1Matches).toHaveLength(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('landing page includes JSON-LD script via component', () => {
|
|
113
|
+
const page = generateWebsiteLandingPage('deploy-ai', mockContext);
|
|
114
|
+
expect(page).toContain('JsonLd');
|
|
115
|
+
expect(page).toContain('ORG_SCHEMA');
|
|
116
|
+
expect(page).toContain('PRODUCT_SCHEMA');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('landing page includes OpenGraph meta via layout', () => {
|
|
120
|
+
const layout = generateWebsiteLayout('deploy-ai', mockContext);
|
|
121
|
+
expect(layout).toContain('openGraph');
|
|
122
|
+
expect(layout).toContain('twitter');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('layout includes metadataBase for canonical URLs', () => {
|
|
126
|
+
const layout = generateWebsiteLayout('deploy-ai', mockContext);
|
|
127
|
+
expect(layout).toContain('metadataBase');
|
|
128
|
+
expect(layout).toContain('NEXT_PUBLIC_SITE_URL');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('sitemap includes all strategy pages', () => {
|
|
132
|
+
const sitemap = generateEnhancedSitemap('deploy-ai', mockStrategy);
|
|
133
|
+
expect(sitemap).toContain('/pricing');
|
|
134
|
+
expect(sitemap).toContain('/docs');
|
|
135
|
+
// Landing page has empty path
|
|
136
|
+
expect(sitemap).toContain('baseUrl');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('landing page uses strategy headline not generic text', () => {
|
|
140
|
+
const page = generateWebsiteLandingPage('deploy-ai', mockContext);
|
|
141
|
+
expect(page).toContain('Ship Code 10x Faster');
|
|
142
|
+
expect(page).not.toContain('Welcome to');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('landing page includes trust signals when strategy provides them', () => {
|
|
146
|
+
const page = generateWebsiteLandingPage('deploy-ai', mockContext);
|
|
147
|
+
expect(page).toContain('SOC 2 Compliant');
|
|
148
|
+
expect(page).toContain('GDPR Ready');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('landing page includes social proof when strategy provides it', () => {
|
|
152
|
+
const page = generateWebsiteLandingPage('deploy-ai', mockContext);
|
|
153
|
+
expect(page).toContain('500+ engineering teams');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('pricing page has proper H1 tag', () => {
|
|
157
|
+
const page = generateWebsitePricingPage('deploy-ai', mockContext);
|
|
158
|
+
const h1Matches = page.match(/<h1[\s>]/g);
|
|
159
|
+
expect(h1Matches).toHaveLength(1);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('pricing page includes enterprise CTA', () => {
|
|
163
|
+
const page = generateWebsitePricingPage('deploy-ai', mockContext);
|
|
164
|
+
expect(page).toContain('Need a custom plan');
|
|
165
|
+
expect(page).toContain('/contact');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('Accessibility Quality Gates', () => {
|
|
170
|
+
it('logo image has alt text in header', () => {
|
|
171
|
+
const contextWithLogo: WebsiteContentContext = {
|
|
172
|
+
...mockContext,
|
|
173
|
+
brandAssets: {
|
|
174
|
+
logoPath: '/path/to/logo.svg',
|
|
175
|
+
logoOutputPath: 'public/brand/logo.svg',
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
const header = generateWebsiteHeader('deploy-ai', contextWithLogo, mockStrategy);
|
|
179
|
+
expect(header).toContain('alt="DeployAI"');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('CTA buttons have accessible text', () => {
|
|
183
|
+
const page = generateWebsiteLandingPage('deploy-ai', mockContext);
|
|
184
|
+
expect(page).toContain('Start Free Trial');
|
|
185
|
+
expect(page).toContain('Read Docs');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('heading hierarchy is sequential (no h1 -> h3 skip)', () => {
|
|
189
|
+
const page = generateWebsiteLandingPage('deploy-ai', mockContext);
|
|
190
|
+
// After h1, should use h2 (not h3)
|
|
191
|
+
const headings = [...page.matchAll(/<h(\d)/g)].map(m => parseInt(m[1], 10));
|
|
192
|
+
for (let i = 1; i < headings.length; i++) {
|
|
193
|
+
// Each heading should not skip more than 1 level
|
|
194
|
+
expect(headings[i] - headings[i - 1]).toBeLessThanOrEqual(1);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('mobile menu button has aria-label', () => {
|
|
199
|
+
const header = generateWebsiteHeader('deploy-ai', mockContext, mockStrategy);
|
|
200
|
+
expect(header).toContain('aria-label');
|
|
201
|
+
expect(header).toContain('aria-expanded');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('404 page has back-to-home link', () => {
|
|
205
|
+
const page = generate404Page('deploy-ai', mockContext);
|
|
206
|
+
expect(page).toContain('href="/"');
|
|
207
|
+
expect(page).toContain('DeployAI');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('500 page has try-again action', () => {
|
|
211
|
+
const page = generate500Page('deploy-ai');
|
|
212
|
+
expect(page).toContain('reset()');
|
|
213
|
+
expect(page).toContain('Try again');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('SEO Component Quality', () => {
|
|
218
|
+
it('JsonLd component renders structured data', () => {
|
|
219
|
+
const component = generateJsonLdComponent();
|
|
220
|
+
expect(component).toContain('application/ld+json');
|
|
221
|
+
expect(component).toContain('dangerouslySetInnerHTML');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('web manifest has correct structure', () => {
|
|
225
|
+
const manifest = generateWebManifest('deploy-ai', mockContext);
|
|
226
|
+
const parsed = JSON.parse(manifest);
|
|
227
|
+
expect(parsed.name).toBe('DeployAI');
|
|
228
|
+
expect(parsed.theme_color).toBe('#2563EB');
|
|
229
|
+
expect(parsed.icons).toHaveLength(3);
|
|
230
|
+
expect(parsed.display).toBe('standalone');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('meta helper generates canonical URLs', () => {
|
|
234
|
+
const helper = generateMetaHelper('deploy-ai', mockStrategy);
|
|
235
|
+
expect(helper).toContain('canonical');
|
|
236
|
+
expect(helper).toContain('NEXT_PUBLIC_SITE_URL');
|
|
237
|
+
expect(helper).toContain('CI/CD');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('footer includes all strategy sections', () => {
|
|
241
|
+
const footer = generateWebsiteFooter('deploy-ai', mockContext, mockStrategy);
|
|
242
|
+
expect(footer).toContain('Product');
|
|
243
|
+
expect(footer).toContain('Legal');
|
|
244
|
+
expect(footer).toContain('Privacy');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for workspace root detection
|
|
3
|
+
* Verifies .popeye/ detection, workspaces in package.json, and fallback behavior
|
|
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 { resolveWorkspaceRoot, getScanDirectories } from '../../src/generators/workspace-root.js';
|
|
11
|
+
|
|
12
|
+
describe('resolveWorkspaceRoot', () => {
|
|
13
|
+
let tmpDir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-ws-root-'));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('detects .popeye/ directory as workspace root', async () => {
|
|
24
|
+
// Create nested structure: tmpDir/project/.popeye/ with cwd = tmpDir/project/sub
|
|
25
|
+
const projectDir = path.join(tmpDir, 'project');
|
|
26
|
+
const subDir = path.join(projectDir, 'sub');
|
|
27
|
+
await fs.mkdir(path.join(projectDir, '.popeye'), { recursive: true });
|
|
28
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
29
|
+
|
|
30
|
+
const root = await resolveWorkspaceRoot(subDir);
|
|
31
|
+
|
|
32
|
+
expect(root).toBe(projectDir);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('detects package.json with workspaces field', async () => {
|
|
36
|
+
const projectDir = path.join(tmpDir, 'monorepo');
|
|
37
|
+
const subDir = path.join(projectDir, 'apps', 'website');
|
|
38
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
39
|
+
await fs.writeFile(
|
|
40
|
+
path.join(projectDir, 'package.json'),
|
|
41
|
+
JSON.stringify({ name: 'root', workspaces: ['apps/*', 'packages/*'] })
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const root = await resolveWorkspaceRoot(subDir);
|
|
45
|
+
|
|
46
|
+
expect(root).toBe(projectDir);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('falls back to cwd when no workspace indicators found', async () => {
|
|
50
|
+
const cwd = path.join(tmpDir, 'standalone');
|
|
51
|
+
await fs.mkdir(cwd, { recursive: true });
|
|
52
|
+
|
|
53
|
+
const root = await resolveWorkspaceRoot(cwd);
|
|
54
|
+
|
|
55
|
+
expect(root).toBe(cwd);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('detects turbo.json or pnpm-workspace.yaml', async () => {
|
|
59
|
+
const projectDir = path.join(tmpDir, 'turborepo');
|
|
60
|
+
const subDir = path.join(projectDir, 'apps', 'web');
|
|
61
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
62
|
+
await fs.writeFile(path.join(projectDir, 'turbo.json'), '{}');
|
|
63
|
+
|
|
64
|
+
const root = await resolveWorkspaceRoot(subDir);
|
|
65
|
+
|
|
66
|
+
expect(root).toBe(projectDir);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('getScanDirectories', () => {
|
|
71
|
+
let tmpDir: string;
|
|
72
|
+
|
|
73
|
+
beforeEach(async () => {
|
|
74
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-scan-'));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterEach(async () => {
|
|
78
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('includes workspace root, parent, and subdirectories', async () => {
|
|
82
|
+
const projectDir = path.join(tmpDir, 'project');
|
|
83
|
+
await fs.mkdir(path.join(projectDir, '.popeye'), { recursive: true });
|
|
84
|
+
await fs.mkdir(path.join(projectDir, 'docs'), { recursive: true });
|
|
85
|
+
await fs.mkdir(path.join(projectDir, 'brand'), { recursive: true });
|
|
86
|
+
|
|
87
|
+
const dirs = await getScanDirectories(projectDir);
|
|
88
|
+
|
|
89
|
+
expect(dirs).toContain(projectDir);
|
|
90
|
+
expect(dirs).toContain(tmpDir); // parent
|
|
91
|
+
expect(dirs).toContain(path.join(projectDir, 'docs'));
|
|
92
|
+
expect(dirs).toContain(path.join(projectDir, 'brand'));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('deduplicates directories', async () => {
|
|
96
|
+
const projectDir = path.join(tmpDir, 'project');
|
|
97
|
+
await fs.mkdir(projectDir, { recursive: true });
|
|
98
|
+
|
|
99
|
+
const dirs = await getScanDirectories(projectDir);
|
|
100
|
+
|
|
101
|
+
// No duplicates
|
|
102
|
+
const unique = new Set(dirs);
|
|
103
|
+
expect(unique.size).toBe(dirs.length);
|
|
104
|
+
});
|
|
105
|
+
});
|