popeye-cli 1.4.7 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +222 -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/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +42 -0
- 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 +2 -1
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +49 -0
- package/dist/generators/doc-parser.d.ts.map +1 -0
- package/dist/generators/doc-parser.js +336 -0
- package/dist/generators/doc-parser.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 +33 -0
- package/dist/generators/templates/website-components.d.ts.map +1 -0
- package/dist/generators/templates/website-components.js +278 -0
- package/dist/generators/templates/website-components.js.map +1 -0
- package/dist/generators/templates/website-config.d.ts +41 -0
- package/dist/generators/templates/website-config.d.ts.map +1 -0
- package/dist/generators/templates/website-config.js +283 -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-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 +14 -47
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +412 -499
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-context.d.ts +83 -0
- package/dist/generators/website-context.d.ts.map +1 -0
- package/dist/generators/website-context.js +190 -0
- package/dist/generators/website-context.js.map +1 -0
- package/dist/generators/website.d.ts +3 -0
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +73 -10
- package/dist/generators/website.js.map +1 -1
- package/dist/state/index.d.ts +27 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +30 -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 +15 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +6 -0
- package/dist/types/workflow.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 +3 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +25 -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 +354 -0
- package/dist/workflow/overview.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +2 -1
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +83 -5
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts +70 -0
- package/dist/workflow/website-strategy.d.ts.map +1 -0
- package/dist/workflow/website-strategy.js +238 -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 +105 -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/interactive.ts +47 -0
- package/src/generators/all.ts +6 -1
- package/src/generators/doc-parser.ts +372 -0
- package/src/generators/templates/index.ts +4 -0
- package/src/generators/templates/website-components.ts +305 -0
- package/src/generators/templates/website-config.ts +291 -0
- package/src/generators/templates/website-conversion.ts +341 -0
- package/src/generators/templates/website-seo.ts +370 -0
- package/src/generators/templates/website.ts +451 -505
- package/src/generators/website-context.ts +265 -0
- package/src/generators/website.ts +109 -19
- package/src/state/index.ts +42 -0
- package/src/types/consensus.ts +3 -0
- package/src/types/website-strategy.ts +243 -0
- package/src/types/workflow.ts +15 -0
- package/src/workflow/consensus.ts +2 -0
- package/src/workflow/execution-mode.ts +21 -0
- package/src/workflow/index.ts +25 -0
- package/src/workflow/overview.ts +469 -0
- package/src/workflow/plan-mode.ts +115 -4
- package/src/workflow/website-strategy.ts +305 -0
- package/src/workflow/website-updater.ts +131 -0
- package/src/workflow/workflow-logger.ts +1 -0
- package/tests/adapters/persona-switching.test.ts +63 -0
- package/tests/generators/website-components.test.ts +159 -0
- package/tests/generators/website-context.test.ts +222 -0
- package/tests/generators/website-seo-quality.test.ts +246 -0
- package/tests/workflow/overview.test.ts +392 -0
- package/tests/workflow/website-strategy.test.ts +191 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for website-context module
|
|
3
|
+
* Verifies doc discovery, brand asset detection, and context building
|
|
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 {
|
|
11
|
+
discoverProjectDocs,
|
|
12
|
+
readProjectDocs,
|
|
13
|
+
findBrandAssets,
|
|
14
|
+
buildWebsiteContext,
|
|
15
|
+
} from '../../src/generators/website-context.js';
|
|
16
|
+
|
|
17
|
+
describe('discoverProjectDocs', () => {
|
|
18
|
+
let tmpDir: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-test-'));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('finds .md files matching spec/pricing/color patterns', async () => {
|
|
29
|
+
await fs.writeFile(path.join(tmpDir, 'product-spec.md'), '# Product Spec');
|
|
30
|
+
await fs.writeFile(path.join(tmpDir, 'pricing.md'), '# Pricing');
|
|
31
|
+
await fs.writeFile(path.join(tmpDir, 'color-scheme.md'), '# Colors');
|
|
32
|
+
await fs.writeFile(path.join(tmpDir, 'random-notes.md'), '# Notes');
|
|
33
|
+
|
|
34
|
+
const docs = await discoverProjectDocs(tmpDir);
|
|
35
|
+
|
|
36
|
+
expect(docs.length).toBe(3);
|
|
37
|
+
expect(docs.some((d) => d.includes('product-spec.md'))).toBe(true);
|
|
38
|
+
expect(docs.some((d) => d.includes('pricing.md'))).toBe(true);
|
|
39
|
+
expect(docs.some((d) => d.includes('color-scheme.md'))).toBe(true);
|
|
40
|
+
// random-notes.md should NOT match
|
|
41
|
+
expect(docs.some((d) => d.includes('random-notes.md'))).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('ignores node_modules and .popeye directories', async () => {
|
|
45
|
+
await fs.mkdir(path.join(tmpDir, 'node_modules'), { recursive: true });
|
|
46
|
+
await fs.writeFile(
|
|
47
|
+
path.join(tmpDir, 'node_modules', 'spec.md'),
|
|
48
|
+
'# Should be ignored'
|
|
49
|
+
);
|
|
50
|
+
await fs.mkdir(path.join(tmpDir, '.popeye'), { recursive: true });
|
|
51
|
+
await fs.writeFile(
|
|
52
|
+
path.join(tmpDir, '.popeye', 'spec.md'),
|
|
53
|
+
'# Should be ignored'
|
|
54
|
+
);
|
|
55
|
+
await fs.writeFile(path.join(tmpDir, 'real-spec.md'), '# Real spec');
|
|
56
|
+
|
|
57
|
+
const docs = await discoverProjectDocs(tmpDir);
|
|
58
|
+
|
|
59
|
+
expect(docs.length).toBe(1);
|
|
60
|
+
expect(docs[0]).toContain('real-spec.md');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns empty for directory with no docs', async () => {
|
|
64
|
+
await fs.writeFile(path.join(tmpDir, 'index.ts'), 'export const x = 1;');
|
|
65
|
+
|
|
66
|
+
const docs = await discoverProjectDocs(tmpDir);
|
|
67
|
+
|
|
68
|
+
expect(docs).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('scans docs/ subdirectory', async () => {
|
|
72
|
+
await fs.mkdir(path.join(tmpDir, 'docs'), { recursive: true });
|
|
73
|
+
await fs.writeFile(
|
|
74
|
+
path.join(tmpDir, 'docs', 'api-guide.md'),
|
|
75
|
+
'# API Guide'
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const docs = await discoverProjectDocs(tmpDir);
|
|
79
|
+
|
|
80
|
+
expect(docs.length).toBe(1);
|
|
81
|
+
expect(docs[0]).toContain('api-guide.md');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('readProjectDocs', () => {
|
|
86
|
+
let tmpDir: string;
|
|
87
|
+
|
|
88
|
+
beforeEach(async () => {
|
|
89
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-test-'));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(async () => {
|
|
93
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('concatenates content with headers and caps at maxLength', async () => {
|
|
97
|
+
const file1 = path.join(tmpDir, 'spec.md');
|
|
98
|
+
const file2 = path.join(tmpDir, 'pricing.md');
|
|
99
|
+
await fs.writeFile(file1, 'Spec content here');
|
|
100
|
+
await fs.writeFile(file2, 'Pricing content here');
|
|
101
|
+
|
|
102
|
+
const result = await readProjectDocs([file1, file2]);
|
|
103
|
+
|
|
104
|
+
expect(result).toContain('--- spec.md ---');
|
|
105
|
+
expect(result).toContain('Spec content here');
|
|
106
|
+
expect(result).toContain('--- pricing.md ---');
|
|
107
|
+
expect(result).toContain('Pricing content here');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('respects maxLength cap', async () => {
|
|
111
|
+
const file1 = path.join(tmpDir, 'big.md');
|
|
112
|
+
await fs.writeFile(file1, 'x'.repeat(10000));
|
|
113
|
+
|
|
114
|
+
const result = await readProjectDocs([file1], 100);
|
|
115
|
+
|
|
116
|
+
expect(result.length).toBeLessThanOrEqual(120); // header + trimmed content
|
|
117
|
+
expect(result).toContain('...');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('findBrandAssets', () => {
|
|
122
|
+
let tmpDir: string;
|
|
123
|
+
|
|
124
|
+
beforeEach(async () => {
|
|
125
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-test-'));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
afterEach(async () => {
|
|
129
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('finds logo PNG/SVG files', async () => {
|
|
133
|
+
await fs.writeFile(path.join(tmpDir, 'Company Logo.png'), 'fake-png');
|
|
134
|
+
|
|
135
|
+
const result = await findBrandAssets(tmpDir);
|
|
136
|
+
|
|
137
|
+
expect(result.logoPath).toBeDefined();
|
|
138
|
+
expect(result.logoPath).toContain('Logo.png');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('returns empty when no logo found', async () => {
|
|
142
|
+
await fs.writeFile(path.join(tmpDir, 'screenshot.png'), 'fake-png');
|
|
143
|
+
|
|
144
|
+
const result = await findBrandAssets(tmpDir);
|
|
145
|
+
|
|
146
|
+
expect(result.logoPath).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('buildWebsiteContext', () => {
|
|
151
|
+
let tmpDir: string;
|
|
152
|
+
|
|
153
|
+
beforeEach(async () => {
|
|
154
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-test-'));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
afterEach(async () => {
|
|
158
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('creates structured context from docs with specification', async () => {
|
|
162
|
+
await fs.writeFile(
|
|
163
|
+
path.join(tmpDir, 'color-scheme.md'),
|
|
164
|
+
'# Colors\nPrimary: #2563EB\n'
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const context = await buildWebsiteContext(
|
|
168
|
+
tmpDir,
|
|
169
|
+
'my-project',
|
|
170
|
+
'# Overview\nA permission-aware retrieval layer for AI systems.\n## Features\n- Access control'
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expect(context.productName).toBe('my-project');
|
|
174
|
+
expect(context.description).toContain('permission-aware');
|
|
175
|
+
expect(context.brand?.primaryColor).toBe('#2563EB');
|
|
176
|
+
expect(context.rawDocs).toContain('color-scheme.md');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('returns minimal context when no docs exist', async () => {
|
|
180
|
+
const context = await buildWebsiteContext(tmpDir, 'empty-project');
|
|
181
|
+
|
|
182
|
+
expect(context.productName).toBe('empty-project');
|
|
183
|
+
expect(context.features).toEqual([]);
|
|
184
|
+
expect(context.rawDocs).toBe('');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('extracts product name, tagline, features, and pricing from rich docs', async () => {
|
|
188
|
+
// Simulate Gateco-like docs (wrapped in code fences like the real files)
|
|
189
|
+
await fs.writeFile(
|
|
190
|
+
path.join(tmpDir, 'Gateco-spec.md'),
|
|
191
|
+
'```md\n# Gateco — Permission-Aware Retrieval for AI Systems\n\n## 2. What Is Gateco?\n\n**Gateco** is a permission-aware retrieval layer that sits between AI agents and vector databases.\n\nIt enforces:\n- organizational permissions\n- identity-based access control\n\n## 3. Core Design Principles\n\n1. **Vector DB agnostic**\n2. **Embedding agnostic**\n3. **Identity-driven** - not prompt-driven\n4. **Late-binding authorization**\n```'
|
|
192
|
+
);
|
|
193
|
+
await fs.writeFile(
|
|
194
|
+
path.join(tmpDir, 'Gateco-pricing.md'),
|
|
195
|
+
'```md\n# Gateco Pricing\n\n## Pricing Overview\n\n| Plan | Price |\n|---|---|\n| **Free (Dev & POC)** | Free |\n| **Pro (Usage-Based)** | $99 / month minimum |\n| **Enterprise** | Custom pricing |\n\n## Plan Positioning\n\n- **Free (Dev & POC)**\n *Build and test safely.*\n\n- **Pro (Usage-Based)**\n *Run production AI workloads.*\n\n- **Enterprise**\n *Deploy in regulated environments.*\n```'
|
|
196
|
+
);
|
|
197
|
+
await fs.writeFile(
|
|
198
|
+
path.join(tmpDir, 'color-scheme.md'),
|
|
199
|
+
'# Colors\n\n| Token | Hex | Usage |\n|---|---|---|\n| `bg-primary` | `#0F172A` | Dark background |\n| `accent-primary` | `#2563EB` | Primary CTA, links |\n'
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const context = await buildWebsiteContext(tmpDir, 'read-all-files');
|
|
203
|
+
|
|
204
|
+
// Product name extracted from spec heading, not folder name
|
|
205
|
+
expect(context.productName).toBe('Gateco');
|
|
206
|
+
// Tagline from "— tagline" pattern
|
|
207
|
+
expect(context.tagline).toContain('Permission-Aware Retrieval');
|
|
208
|
+
// Description from "What Is Gateco?" section
|
|
209
|
+
expect(context.description).toContain('permission-aware retrieval layer');
|
|
210
|
+
// Features extracted from Core Design Principles
|
|
211
|
+
expect(context.features.length).toBeGreaterThanOrEqual(2);
|
|
212
|
+
expect(context.features.some((f) => f.title.includes('Vector DB'))).toBe(true);
|
|
213
|
+
// Pricing tiers extracted
|
|
214
|
+
expect(context.pricing).toBeDefined();
|
|
215
|
+
expect(context.pricing!.length).toBe(3);
|
|
216
|
+
expect(context.pricing![0].name).toContain('Free');
|
|
217
|
+
expect(context.pricing![1].price).toBe('$99');
|
|
218
|
+
expect(context.pricing![2].price).toBe('Custom');
|
|
219
|
+
// Primary color is accent-primary (#2563EB), NOT bg-primary (#0F172A)
|
|
220
|
+
expect(context.brand?.primaryColor).toBe('#2563EB');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -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
|
+
});
|