popeye-cli 2.2.0 → 2.7.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/dist/adapters/gemini.d.ts +14 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +41 -6
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/grok.d.ts +14 -0
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +42 -6
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/openai.d.ts +10 -0
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +44 -5
- package/dist/adapters/openai.js.map +1 -1
- package/dist/cli/commands/create.js +1 -1
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +324 -20
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +3 -2
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +21 -6
- package/dist/generators/doc-parser.d.ts.map +1 -1
- package/dist/generators/doc-parser.js +55 -4
- package/dist/generators/doc-parser.js.map +1 -1
- package/dist/generators/templates/fullstack.js +1 -1
- package/dist/generators/templates/website-components.js +1 -1
- package/dist/generators/templates/website-components.js.map +1 -1
- package/dist/generators/templates/website-config.d.ts +4 -1
- package/dist/generators/templates/website-config.d.ts.map +1 -1
- package/dist/generators/templates/website-config.js +17 -11
- package/dist/generators/templates/website-config.js.map +1 -1
- package/dist/generators/templates/website-conversion.js +1 -1
- package/dist/generators/templates/website-conversion.js.map +1 -1
- package/dist/generators/templates/website-landing.js +1 -1
- package/dist/generators/templates/website-landing.js.map +1 -1
- package/dist/generators/templates/website-layout.d.ts +36 -4
- package/dist/generators/templates/website-layout.d.ts.map +1 -1
- package/dist/generators/templates/website-layout.js +466 -23
- package/dist/generators/templates/website-layout.js.map +1 -1
- package/dist/generators/templates/website-pricing.js +1 -1
- package/dist/generators/templates/website-pricing.js.map +1 -1
- package/dist/generators/templates/website-sections.js +1 -1
- package/dist/generators/templates/website-sections.js.map +1 -1
- package/dist/generators/templates/website-seo.d.ts.map +1 -1
- package/dist/generators/templates/website-seo.js +4 -1
- package/dist/generators/templates/website-seo.js.map +1 -1
- package/dist/generators/templates/website.d.ts +1 -1
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +1 -1
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-content-ai.d.ts +52 -0
- package/dist/generators/website-content-ai.d.ts.map +1 -0
- package/dist/generators/website-content-ai.js +141 -0
- package/dist/generators/website-content-ai.js.map +1 -0
- package/dist/generators/website-content-scanner.d.ts +1 -1
- package/dist/generators/website-content-scanner.d.ts.map +1 -1
- package/dist/generators/website-content-scanner.js +98 -1
- package/dist/generators/website-content-scanner.js.map +1 -1
- package/dist/generators/website-context.d.ts +34 -1
- package/dist/generators/website-context.d.ts.map +1 -1
- package/dist/generators/website-context.js +131 -9
- package/dist/generators/website-context.js.map +1 -1
- package/dist/generators/website-debug.d.ts +12 -0
- package/dist/generators/website-debug.d.ts.map +1 -1
- package/dist/generators/website-debug.js +16 -0
- package/dist/generators/website-debug.js.map +1 -1
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +26 -4
- package/dist/generators/website.js.map +1 -1
- package/dist/pipeline/auto-recovery.d.ts +56 -0
- package/dist/pipeline/auto-recovery.d.ts.map +1 -0
- package/dist/pipeline/auto-recovery.js +185 -0
- package/dist/pipeline/auto-recovery.js.map +1 -0
- package/dist/pipeline/change-request.d.ts +39 -0
- package/dist/pipeline/change-request.d.ts.map +1 -1
- package/dist/pipeline/change-request.js +40 -1
- package/dist/pipeline/change-request.js.map +1 -1
- package/dist/pipeline/check-runner.d.ts +30 -1
- package/dist/pipeline/check-runner.d.ts.map +1 -1
- package/dist/pipeline/check-runner.js +122 -1
- package/dist/pipeline/check-runner.js.map +1 -1
- package/dist/pipeline/command-resolver.d.ts.map +1 -1
- package/dist/pipeline/command-resolver.js +33 -2
- package/dist/pipeline/command-resolver.js.map +1 -1
- package/dist/pipeline/consensus/arbitrator-query.d.ts +22 -0
- package/dist/pipeline/consensus/arbitrator-query.d.ts.map +1 -0
- package/dist/pipeline/consensus/arbitrator-query.js +70 -0
- package/dist/pipeline/consensus/arbitrator-query.js.map +1 -0
- package/dist/pipeline/consensus/consensus-runner.d.ts +131 -7
- package/dist/pipeline/consensus/consensus-runner.d.ts.map +1 -1
- package/dist/pipeline/consensus/consensus-runner.js +809 -35
- package/dist/pipeline/consensus/consensus-runner.js.map +1 -1
- package/dist/pipeline/cr-lifecycle.d.ts +42 -0
- package/dist/pipeline/cr-lifecycle.d.ts.map +1 -0
- package/dist/pipeline/cr-lifecycle.js +89 -0
- package/dist/pipeline/cr-lifecycle.js.map +1 -0
- package/dist/pipeline/gate-engine.d.ts +1 -0
- package/dist/pipeline/gate-engine.d.ts.map +1 -1
- package/dist/pipeline/gate-engine.js +26 -7
- package/dist/pipeline/gate-engine.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +306 -16
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/packets/consensus-packet-builder.d.ts +15 -4
- package/dist/pipeline/packets/consensus-packet-builder.d.ts.map +1 -1
- package/dist/pipeline/packets/consensus-packet-builder.js +29 -17
- package/dist/pipeline/packets/consensus-packet-builder.js.map +1 -1
- package/dist/pipeline/phases/architecture.d.ts.map +1 -1
- package/dist/pipeline/phases/architecture.js +5 -3
- package/dist/pipeline/phases/architecture.js.map +1 -1
- package/dist/pipeline/phases/audit.d.ts.map +1 -1
- package/dist/pipeline/phases/audit.js +5 -3
- package/dist/pipeline/phases/audit.js.map +1 -1
- package/dist/pipeline/phases/consensus-architecture.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-architecture.js +10 -1
- package/dist/pipeline/phases/consensus-architecture.js.map +1 -1
- package/dist/pipeline/phases/consensus-master-plan.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-master-plan.js +10 -3
- package/dist/pipeline/phases/consensus-master-plan.js.map +1 -1
- package/dist/pipeline/phases/consensus-role-plans.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-role-plans.js +10 -1
- package/dist/pipeline/phases/consensus-role-plans.js.map +1 -1
- package/dist/pipeline/phases/done.d.ts.map +1 -1
- package/dist/pipeline/phases/done.js +9 -4
- package/dist/pipeline/phases/done.js.map +1 -1
- package/dist/pipeline/phases/intake.d.ts.map +1 -1
- package/dist/pipeline/phases/intake.js +7 -3
- package/dist/pipeline/phases/intake.js.map +1 -1
- package/dist/pipeline/phases/phase-context.d.ts +2 -0
- package/dist/pipeline/phases/phase-context.d.ts.map +1 -1
- package/dist/pipeline/phases/phase-context.js +3 -1
- package/dist/pipeline/phases/phase-context.js.map +1 -1
- package/dist/pipeline/phases/production-gate.d.ts.map +1 -1
- package/dist/pipeline/phases/production-gate.js +28 -3
- package/dist/pipeline/phases/production-gate.js.map +1 -1
- package/dist/pipeline/phases/qa-validation.d.ts.map +1 -1
- package/dist/pipeline/phases/qa-validation.js +38 -5
- package/dist/pipeline/phases/qa-validation.js.map +1 -1
- package/dist/pipeline/phases/recovery-loop.d.ts +2 -0
- package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -1
- package/dist/pipeline/phases/recovery-loop.js +200 -6
- package/dist/pipeline/phases/recovery-loop.js.map +1 -1
- package/dist/pipeline/phases/review.d.ts.map +1 -1
- package/dist/pipeline/phases/review.js +58 -28
- package/dist/pipeline/phases/review.js.map +1 -1
- package/dist/pipeline/phases/role-planning.d.ts.map +1 -1
- package/dist/pipeline/phases/role-planning.js +18 -2
- package/dist/pipeline/phases/role-planning.js.map +1 -1
- package/dist/pipeline/phases/stuck.d.ts.map +1 -1
- package/dist/pipeline/phases/stuck.js +10 -0
- package/dist/pipeline/phases/stuck.js.map +1 -1
- package/dist/pipeline/repo-snapshot.d.ts.map +1 -1
- package/dist/pipeline/repo-snapshot.js +3 -0
- package/dist/pipeline/repo-snapshot.js.map +1 -1
- package/dist/pipeline/role-execution-adapter.d.ts +2 -1
- package/dist/pipeline/role-execution-adapter.d.ts.map +1 -1
- package/dist/pipeline/role-execution-adapter.js +22 -7
- package/dist/pipeline/role-execution-adapter.js.map +1 -1
- package/dist/pipeline/skill-loader.d.ts +19 -0
- package/dist/pipeline/skill-loader.d.ts.map +1 -1
- package/dist/pipeline/skill-loader.js +22 -0
- package/dist/pipeline/skill-loader.js.map +1 -1
- package/dist/pipeline/skills/coverage-gate.d.ts +44 -0
- package/dist/pipeline/skills/coverage-gate.d.ts.map +1 -0
- package/dist/pipeline/skills/coverage-gate.js +143 -0
- package/dist/pipeline/skills/coverage-gate.js.map +1 -0
- package/dist/pipeline/skills/usage-registry.d.ts +48 -0
- package/dist/pipeline/skills/usage-registry.d.ts.map +1 -0
- package/dist/pipeline/skills/usage-registry.js +55 -0
- package/dist/pipeline/skills/usage-registry.js.map +1 -0
- package/dist/pipeline/strategy-context.d.ts +20 -0
- package/dist/pipeline/strategy-context.d.ts.map +1 -0
- package/dist/pipeline/strategy-context.js +55 -0
- package/dist/pipeline/strategy-context.js.map +1 -0
- package/dist/pipeline/type-defs/artifacts.d.ts +25 -5
- package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -1
- package/dist/pipeline/type-defs/artifacts.js +4 -0
- package/dist/pipeline/type-defs/artifacts.js.map +1 -1
- package/dist/pipeline/type-defs/audit.d.ts +25 -13
- package/dist/pipeline/type-defs/audit.d.ts.map +1 -1
- package/dist/pipeline/type-defs/checks.d.ts +18 -8
- package/dist/pipeline/type-defs/checks.d.ts.map +1 -1
- package/dist/pipeline/type-defs/checks.js +4 -0
- package/dist/pipeline/type-defs/checks.js.map +1 -1
- package/dist/pipeline/type-defs/packets.d.ts +104 -18
- package/dist/pipeline/type-defs/packets.d.ts.map +1 -1
- package/dist/pipeline/type-defs/packets.js +17 -1
- package/dist/pipeline/type-defs/packets.js.map +1 -1
- package/dist/pipeline/type-defs/state.d.ts +160 -16
- package/dist/pipeline/type-defs/state.d.ts.map +1 -1
- package/dist/pipeline/type-defs/state.js +26 -1
- package/dist/pipeline/type-defs/state.js.map +1 -1
- package/dist/shared/text-utils.d.ts +23 -0
- package/dist/shared/text-utils.d.ts.map +1 -0
- package/dist/shared/text-utils.js +66 -0
- package/dist/shared/text-utils.js.map +1 -0
- package/dist/shared/website-strategy-format.d.ts +18 -0
- package/dist/shared/website-strategy-format.d.ts.map +1 -0
- package/dist/shared/website-strategy-format.js +47 -0
- package/dist/shared/website-strategy-format.js.map +1 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +57 -8
- package/dist/state/index.js.map +1 -1
- package/dist/types/consensus.d.ts +1 -0
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/website-strategy.d.ts +1 -1
- package/dist/types/workflow.d.ts +447 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +3 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.d.ts.map +1 -1
- package/dist/upgrade/handlers.js +6 -3
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +1 -0
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts.map +1 -1
- package/dist/workflow/website-strategy.js +2 -29
- 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 +3 -2
- package/dist/workflow/website-updater.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/gemini.ts +51 -6
- package/src/adapters/grok.ts +51 -6
- package/src/adapters/openai.ts +53 -5
- package/src/cli/commands/create.ts +1 -1
- package/src/cli/interactive.ts +333 -19
- package/src/generators/all.ts +3 -2
- package/src/generators/doc-parser.ts +75 -15
- package/src/generators/templates/fullstack.ts +1 -1
- package/src/generators/templates/website-components.ts +1 -1
- package/src/generators/templates/website-config.ts +23 -11
- package/src/generators/templates/website-conversion.ts +1 -1
- package/src/generators/templates/website-landing.ts +1 -1
- package/src/generators/templates/website-layout.ts +491 -23
- package/src/generators/templates/website-pricing.ts +1 -1
- package/src/generators/templates/website-sections.ts +1 -1
- package/src/generators/templates/website-seo.ts +4 -1
- package/src/generators/templates/website.ts +3 -0
- package/src/generators/website-content-ai.ts +186 -0
- package/src/generators/website-content-scanner.ts +113 -1
- package/src/generators/website-context.ts +151 -12
- package/src/generators/website-debug.ts +26 -0
- package/src/generators/website.ts +28 -3
- package/src/pipeline/auto-recovery.ts +283 -0
- package/src/pipeline/change-request.ts +63 -1
- package/src/pipeline/check-runner.ts +141 -2
- package/src/pipeline/command-resolver.ts +34 -2
- package/src/pipeline/consensus/arbitrator-query.ts +101 -0
- package/src/pipeline/consensus/consensus-runner.ts +1099 -42
- package/src/pipeline/cr-lifecycle.ts +103 -0
- package/src/pipeline/gate-engine.ts +35 -7
- package/src/pipeline/orchestrator.ts +361 -16
- package/src/pipeline/packets/consensus-packet-builder.ts +44 -18
- package/src/pipeline/phases/architecture.ts +6 -3
- package/src/pipeline/phases/audit.ts +6 -3
- package/src/pipeline/phases/consensus-architecture.ts +10 -1
- package/src/pipeline/phases/consensus-master-plan.ts +10 -3
- package/src/pipeline/phases/consensus-role-plans.ts +10 -1
- package/src/pipeline/phases/done.ts +15 -4
- package/src/pipeline/phases/intake.ts +7 -3
- package/src/pipeline/phases/phase-context.ts +6 -1
- package/src/pipeline/phases/production-gate.ts +41 -3
- package/src/pipeline/phases/qa-validation.ts +51 -5
- package/src/pipeline/phases/recovery-loop.ts +229 -7
- package/src/pipeline/phases/review.ts +73 -30
- package/src/pipeline/phases/role-planning.ts +21 -2
- package/src/pipeline/phases/stuck.ts +10 -0
- package/src/pipeline/repo-snapshot.ts +3 -0
- package/src/pipeline/role-execution-adapter.ts +30 -4
- package/src/pipeline/skill-loader.ts +33 -0
- package/src/pipeline/skills/coverage-gate.ts +199 -0
- package/src/pipeline/skills/usage-registry.ts +87 -0
- package/src/pipeline/strategy-context.ts +60 -0
- package/src/pipeline/type-defs/artifacts.ts +4 -0
- package/src/pipeline/type-defs/checks.ts +4 -0
- package/src/pipeline/type-defs/packets.ts +18 -1
- package/src/pipeline/type-defs/state.ts +26 -1
- package/src/shared/text-utils.ts +70 -0
- package/src/shared/website-strategy-format.ts +56 -0
- package/src/state/index.ts +60 -8
- package/src/types/consensus.ts +1 -0
- package/src/types/workflow.ts +6 -0
- package/src/upgrade/handlers.ts +9 -3
- package/src/workflow/consensus.ts +1 -0
- package/src/workflow/website-strategy.ts +2 -36
- package/src/workflow/website-updater.ts +4 -2
- package/tests/adapters/gemini.test.ts +165 -0
- package/tests/adapters/grok.test.ts +137 -0
- package/tests/adapters/openai.test.ts +128 -0
- package/tests/generators/doc-parser.test.ts +88 -9
- package/tests/generators/quality-gate.test.ts +19 -3
- package/tests/generators/website-components.test.ts +34 -0
- package/tests/generators/website-content-ai.test.ts +308 -0
- package/tests/generators/website-content-scanner.test.ts +86 -0
- package/tests/generators/website-context.test.ts +3 -2
- package/tests/integration/smokestack-scaffold.test.ts +385 -0
- package/tests/pipeline/auto-recovery.test.ts +337 -0
- package/tests/pipeline/change-request.test.ts +70 -0
- package/tests/pipeline/command-resolver.test.ts +42 -0
- package/tests/pipeline/consensus/arbitrator-query.test.ts +107 -0
- package/tests/pipeline/consensus-runner.test.ts +1333 -10
- package/tests/pipeline/consensus-scoring.test.ts +602 -18
- package/tests/pipeline/gate-engine.test.ts +34 -0
- package/tests/pipeline/install-check.test.ts +261 -0
- package/tests/pipeline/orchestrator.test.ts +1506 -15
- package/tests/pipeline/packets/builders.test.ts +29 -6
- package/tests/pipeline/phases/role-planning.strategy.test.ts +204 -0
- package/tests/pipeline/pipeline-persistence.test.ts +230 -0
- package/tests/pipeline/recovery-loop-guidance.test.ts +280 -0
- package/tests/pipeline/role-execution-adapter.test.ts +88 -0
- package/tests/pipeline/skills/coverage-gate.test.ts +370 -0
- package/tests/pipeline/skills/usage-registry.test.ts +114 -0
- package/tests/pipeline/strategy-context.test.ts +148 -0
- package/tests/shared/text-utils.test.ts +155 -0
- package/tests/state/progress-analysis.test.ts +375 -0
- package/tests/upgrade/handlers.test.ts +33 -2
- package/tests/workflow/consensus.test.ts +6 -0
- package/tsconfig.json +1 -1
|
@@ -22,6 +22,9 @@ CONCERNS:
|
|
|
22
22
|
- No deployment plan
|
|
23
23
|
- Security considerations not addressed
|
|
24
24
|
|
|
25
|
+
BLOCKING_ISSUES:
|
|
26
|
+
- None
|
|
27
|
+
|
|
25
28
|
RECOMMENDATIONS:
|
|
26
29
|
- Add error handling middleware
|
|
27
30
|
- Include deployment configuration
|
|
@@ -37,6 +40,7 @@ CONSENSUS: 85%
|
|
|
37
40
|
expect(result.analysis).toContain('well-structured plan');
|
|
38
41
|
expect(result.strengths).toContain('Clear project structure');
|
|
39
42
|
expect(result.concerns).toContain('Missing error handling strategy');
|
|
43
|
+
expect(result.blockingIssues).toEqual([]);
|
|
40
44
|
expect(result.recommendations).toContain('Add error handling middleware');
|
|
41
45
|
expect(result.rawResponse).toBe(response);
|
|
42
46
|
});
|
|
@@ -52,6 +56,9 @@ STRENGTHS:
|
|
|
52
56
|
CONCERNS:
|
|
53
57
|
- None
|
|
54
58
|
|
|
59
|
+
BLOCKING_ISSUES:
|
|
60
|
+
- None
|
|
61
|
+
|
|
55
62
|
RECOMMENDATIONS:
|
|
56
63
|
- None needed
|
|
57
64
|
|
|
@@ -62,6 +69,7 @@ CONSENSUS: 98%
|
|
|
62
69
|
|
|
63
70
|
expect(result.score).toBe(98);
|
|
64
71
|
expect(result.approved).toBe(true);
|
|
72
|
+
expect(result.blockingIssues).toEqual([]);
|
|
65
73
|
});
|
|
66
74
|
|
|
67
75
|
it('should handle missing sections gracefully', () => {
|
|
@@ -78,6 +86,7 @@ CONSENSUS: 70%
|
|
|
78
86
|
expect(result.analysis).toBe('');
|
|
79
87
|
expect(result.strengths).toEqual([]);
|
|
80
88
|
expect(result.concerns).toEqual([]);
|
|
89
|
+
expect(result.blockingIssues).toEqual([]);
|
|
81
90
|
});
|
|
82
91
|
|
|
83
92
|
it('should handle missing score', () => {
|
|
@@ -110,6 +119,9 @@ CONCERNS:
|
|
|
110
119
|
1. First concern
|
|
111
120
|
2. Second concern
|
|
112
121
|
|
|
122
|
+
BLOCKING_ISSUES:
|
|
123
|
+
- None
|
|
124
|
+
|
|
113
125
|
RECOMMENDATIONS:
|
|
114
126
|
- Recommendation one
|
|
115
127
|
- Recommendation two
|
|
@@ -121,6 +133,7 @@ CONSENSUS: 80%
|
|
|
121
133
|
|
|
122
134
|
expect(result.strengths).toHaveLength(4);
|
|
123
135
|
expect(result.concerns).toHaveLength(2);
|
|
136
|
+
expect(result.blockingIssues).toEqual([]);
|
|
124
137
|
expect(result.recommendations).toHaveLength(2);
|
|
125
138
|
});
|
|
126
139
|
|
|
@@ -142,4 +155,119 @@ CONSENSUS: 80%
|
|
|
142
155
|
expect(parseConsensusResponse('CONSENSUS: 0%').score).toBe(0);
|
|
143
156
|
expect(parseConsensusResponse('CONSENSUS: 100%').score).toBe(100);
|
|
144
157
|
});
|
|
158
|
+
|
|
159
|
+
it('should parse blocking issues as separate field', () => {
|
|
160
|
+
const response = `
|
|
161
|
+
ANALYSIS: Good plan.
|
|
162
|
+
|
|
163
|
+
STRENGTHS:
|
|
164
|
+
- Solid architecture
|
|
165
|
+
|
|
166
|
+
CONCERNS:
|
|
167
|
+
- Consider adding caching
|
|
168
|
+
|
|
169
|
+
BLOCKING_ISSUES:
|
|
170
|
+
- Missing authentication flow
|
|
171
|
+
- No database migration strategy
|
|
172
|
+
|
|
173
|
+
RECOMMENDATIONS:
|
|
174
|
+
- Add caching layer
|
|
175
|
+
|
|
176
|
+
CONSENSUS: 72%
|
|
177
|
+
`;
|
|
178
|
+
const result = parseConsensusResponse(response);
|
|
179
|
+
expect(result.blockingIssues).toEqual([
|
|
180
|
+
'Missing authentication flow',
|
|
181
|
+
'No database migration strategy',
|
|
182
|
+
]);
|
|
183
|
+
expect(result.concerns).toEqual(['Consider adding caching']);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should return empty blocking issues when "None"', () => {
|
|
187
|
+
const response = `
|
|
188
|
+
ANALYSIS: Good plan.
|
|
189
|
+
|
|
190
|
+
STRENGTHS:
|
|
191
|
+
- Solid
|
|
192
|
+
|
|
193
|
+
CONCERNS:
|
|
194
|
+
- Minor naming issues in the module structure
|
|
195
|
+
|
|
196
|
+
BLOCKING_ISSUES:
|
|
197
|
+
- None
|
|
198
|
+
|
|
199
|
+
RECOMMENDATIONS:
|
|
200
|
+
- Improve module names
|
|
201
|
+
|
|
202
|
+
CONSENSUS: 95%
|
|
203
|
+
`;
|
|
204
|
+
const result = parseConsensusResponse(response);
|
|
205
|
+
expect(result.blockingIssues).toEqual([]);
|
|
206
|
+
expect(result.approved).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should filter none-variant blocking issues like "No blocking issues found"', () => {
|
|
210
|
+
const response = `
|
|
211
|
+
ANALYSIS: Good plan.
|
|
212
|
+
|
|
213
|
+
STRENGTHS:
|
|
214
|
+
- Solid architecture
|
|
215
|
+
|
|
216
|
+
CONCERNS:
|
|
217
|
+
- Minor naming issues
|
|
218
|
+
|
|
219
|
+
BLOCKING_ISSUES:
|
|
220
|
+
- No blocking issues found
|
|
221
|
+
|
|
222
|
+
RECOMMENDATIONS:
|
|
223
|
+
- Improve naming
|
|
224
|
+
|
|
225
|
+
CONSENSUS: 92%
|
|
226
|
+
`;
|
|
227
|
+
const result = parseConsensusResponse(response);
|
|
228
|
+
expect(result.blockingIssues).toEqual([]);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should filter "None identified" from blocking issues', () => {
|
|
232
|
+
const response = `
|
|
233
|
+
ANALYSIS: Solid plan overall.
|
|
234
|
+
|
|
235
|
+
STRENGTHS:
|
|
236
|
+
- Good design choices
|
|
237
|
+
|
|
238
|
+
CONCERNS:
|
|
239
|
+
- Consider caching
|
|
240
|
+
|
|
241
|
+
BLOCKING_ISSUES:
|
|
242
|
+
- None identified
|
|
243
|
+
|
|
244
|
+
RECOMMENDATIONS:
|
|
245
|
+
- Add caching layer
|
|
246
|
+
|
|
247
|
+
CONSENSUS: 90%
|
|
248
|
+
`;
|
|
249
|
+
const result = parseConsensusResponse(response);
|
|
250
|
+
expect(result.blockingIssues).toEqual([]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should handle missing BLOCKING_ISSUES section (backward compat)', () => {
|
|
254
|
+
// Old-format response without BLOCKING_ISSUES section
|
|
255
|
+
const response = `
|
|
256
|
+
ANALYSIS: Good plan.
|
|
257
|
+
|
|
258
|
+
STRENGTHS:
|
|
259
|
+
- Fine overall structure
|
|
260
|
+
|
|
261
|
+
CONCERNS:
|
|
262
|
+
- Some concern about performance
|
|
263
|
+
|
|
264
|
+
RECOMMENDATIONS:
|
|
265
|
+
- A recommendation for optimization
|
|
266
|
+
|
|
267
|
+
CONSENSUS: 90%
|
|
268
|
+
`;
|
|
269
|
+
const result = parseConsensusResponse(response);
|
|
270
|
+
expect(result.blockingIssues).toEqual([]);
|
|
271
|
+
expect(result.concerns).toHaveLength(1);
|
|
272
|
+
});
|
|
145
273
|
});
|
|
@@ -104,18 +104,97 @@ describe('isSuspiciousProductName', () => {
|
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
describe('extractPricing', () => {
|
|
107
|
-
it('extracts pricing tiers from markdown table', () => {
|
|
107
|
+
it('extracts pricing tiers from markdown table with provenance', () => {
|
|
108
108
|
const docs = `## Pricing\n| Plan | Price |\n|---|---|\n| Free | Free |\n| Pro | $99/month minimum |\n| Enterprise | Custom pricing |`;
|
|
109
|
-
const
|
|
110
|
-
expect(
|
|
111
|
-
expect(tiers
|
|
112
|
-
expect(tiers
|
|
113
|
-
expect(tiers
|
|
114
|
-
expect(tiers
|
|
109
|
+
const result = extractPricing(docs);
|
|
110
|
+
expect(result.source).toBe('docs');
|
|
111
|
+
expect(result.tiers.length).toBe(3);
|
|
112
|
+
expect(result.tiers[0].name).toContain('Free');
|
|
113
|
+
expect(result.tiers[1].price).toBe('$99');
|
|
114
|
+
expect(result.tiers[2].price).toBe('Custom');
|
|
115
|
+
expect(result.evidence).toBeDefined();
|
|
116
|
+
expect(result.evidence!.extractionMethod).toBe('known_plan_names');
|
|
117
|
+
expect(result.evidence!.matchedRows).toBe(3);
|
|
115
118
|
});
|
|
116
119
|
|
|
117
|
-
it('returns
|
|
120
|
+
it('returns source none when no pricing found', () => {
|
|
118
121
|
const docs = '# Product\nJust a product.';
|
|
119
|
-
|
|
122
|
+
const result = extractPricing(docs);
|
|
123
|
+
expect(result.source).toBe('none');
|
|
124
|
+
expect(result.tiers).toEqual([]);
|
|
125
|
+
expect(result.evidence).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('extracts Gateco-style pricing with emoji-prefixed rows and <br> tags', () => {
|
|
129
|
+
const docs = [
|
|
130
|
+
'## Pricing Overview',
|
|
131
|
+
'| Plan | Price | Users |',
|
|
132
|
+
'|---|---|---|',
|
|
133
|
+
'| Free | Free | Up to 5 |',
|
|
134
|
+
'| Pro | $99/month<br>minimum | Unlimited |',
|
|
135
|
+
'| Enterprise | Custom | Unlimited |',
|
|
136
|
+
].join('\n');
|
|
137
|
+
const result = extractPricing(docs);
|
|
138
|
+
expect(result.source).toBe('docs');
|
|
139
|
+
expect(result.tiers.length).toBe(3);
|
|
140
|
+
expect(result.tiers[0].name).toBe('Free');
|
|
141
|
+
expect(result.tiers[0].price).toBe('Free');
|
|
142
|
+
expect(result.tiers[1].name).toBe('Pro');
|
|
143
|
+
expect(result.tiers[1].price).toBe('$99');
|
|
144
|
+
expect(result.tiers[2].name).toBe('Enterprise');
|
|
145
|
+
expect(result.tiers[2].price).toBe('Custom');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('falls back to table_fallback for nonstandard plan names', () => {
|
|
149
|
+
const docs = [
|
|
150
|
+
'## Pricing',
|
|
151
|
+
'| Plan | Price |',
|
|
152
|
+
'|---|---|',
|
|
153
|
+
'| Hobby | Free |',
|
|
154
|
+
'| Scale | $49/mo |',
|
|
155
|
+
'| Organization | Custom |',
|
|
156
|
+
].join('\n');
|
|
157
|
+
const result = extractPricing(docs);
|
|
158
|
+
expect(result.source).toBe('docs');
|
|
159
|
+
expect(result.tiers.length).toBe(3);
|
|
160
|
+
expect(result.evidence!.extractionMethod).toBe('table_fallback');
|
|
161
|
+
expect(result.tiers[0].name).toBe('Hobby');
|
|
162
|
+
expect(result.tiers[1].name).toBe('Scale');
|
|
163
|
+
expect(result.tiers[2].name).toBe('Organization');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('detects price in 3rd column via header scan', () => {
|
|
167
|
+
const docs = [
|
|
168
|
+
'## Pricing',
|
|
169
|
+
'| Plan | Features | Price |',
|
|
170
|
+
'|---|---|---|',
|
|
171
|
+
'| Hobby | 5 projects | Free |',
|
|
172
|
+
'| Scale | Unlimited | $49/mo |',
|
|
173
|
+
].join('\n');
|
|
174
|
+
const result = extractPricing(docs);
|
|
175
|
+
expect(result.source).toBe('docs');
|
|
176
|
+
expect(result.tiers.length).toBe(2);
|
|
177
|
+
expect(result.tiers[0].price).toBe('Free');
|
|
178
|
+
expect(result.tiers[1].price).toBe('$49');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('skips separator rows and header-like rows in fallback', () => {
|
|
182
|
+
const docs = [
|
|
183
|
+
'## Pricing',
|
|
184
|
+
'| Plan | Price |',
|
|
185
|
+
'|---|---|',
|
|
186
|
+
'| Plan | $10 |', // header-like row, should skip
|
|
187
|
+
'| Hobby | Free |',
|
|
188
|
+
].join('\n');
|
|
189
|
+
const result = extractPricing(docs);
|
|
190
|
+
// "Plan" row should be skipped by the /^(Plan|Tier|Name|Feature)/i filter
|
|
191
|
+
expect(result.tiers.every((t) => t.name !== 'Plan')).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('returns empty tiers for pricing section with no table', () => {
|
|
195
|
+
const docs = '## Pricing\nContact us for pricing details.';
|
|
196
|
+
const result = extractPricing(docs);
|
|
197
|
+
expect(result.source).toBe('none');
|
|
198
|
+
expect(result.tiers).toEqual([]);
|
|
120
199
|
});
|
|
121
200
|
});
|
|
@@ -75,7 +75,7 @@ describe('validateWebsiteContextOrThrow', () => {
|
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
it('still throws when passed is false from validateWebsiteContext', () => {
|
|
78
|
-
// Context with multiple blocking issues
|
|
78
|
+
// Context with multiple blocking issues (strategy absence is not a factor)
|
|
79
79
|
const context = makeContext({
|
|
80
80
|
productName: 'my-app',
|
|
81
81
|
rawDocs: '',
|
|
@@ -87,6 +87,13 @@ describe('validateWebsiteContextOrThrow', () => {
|
|
|
87
87
|
/Website generation blocked/
|
|
88
88
|
);
|
|
89
89
|
});
|
|
90
|
+
|
|
91
|
+
it('does not throw on missing strategy alone', () => {
|
|
92
|
+
// Strategy absence should never cause validation failure
|
|
93
|
+
const context = makeContext({ strategy: undefined });
|
|
94
|
+
|
|
95
|
+
expect(() => validateWebsiteContextOrThrow(context, 'gateco')).not.toThrow();
|
|
96
|
+
});
|
|
90
97
|
});
|
|
91
98
|
|
|
92
99
|
describe('validateWebsiteContext (soft mode)', () => {
|
|
@@ -168,7 +175,7 @@ describe('validateWebsiteContext (soft mode)', () => {
|
|
|
168
175
|
expect(result.warnings.some((w) => /description/i.test(w))).toBe(true);
|
|
169
176
|
});
|
|
170
177
|
|
|
171
|
-
it('clamps score to
|
|
178
|
+
it('clamps score to floor when everything is wrong', () => {
|
|
172
179
|
const context: WebsiteContentContext = {
|
|
173
180
|
productName: 'my-app',
|
|
174
181
|
features: [],
|
|
@@ -177,7 +184,16 @@ describe('validateWebsiteContext (soft mode)', () => {
|
|
|
177
184
|
};
|
|
178
185
|
const result = validateWebsiteContext(context, 'my-app');
|
|
179
186
|
|
|
180
|
-
|
|
187
|
+
// Score is low but no longer 0 since strategy penalty (-15) was removed
|
|
188
|
+
expect(result.contentScore).toBeLessThanOrEqual(15);
|
|
181
189
|
expect(result.passed).toBe(false);
|
|
182
190
|
});
|
|
191
|
+
|
|
192
|
+
it('does not emit strategy-related issues when strategy is undefined', () => {
|
|
193
|
+
const context = makeContext({ strategy: undefined });
|
|
194
|
+
const result = validateWebsiteContext(context, 'gateco');
|
|
195
|
+
|
|
196
|
+
// Strategy absence should not produce any issues or warnings about strategy
|
|
197
|
+
expect(result.issues.some((i) => /strategy/i.test(i))).toBe(false);
|
|
198
|
+
});
|
|
183
199
|
});
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
generateWebsiteFooter,
|
|
10
10
|
generateWebsiteNavigation,
|
|
11
11
|
} from '../../src/generators/templates/website-components.js';
|
|
12
|
+
import { getWebsiteProjectFiles } from '../../src/generators/website.js';
|
|
12
13
|
import type { WebsiteContentContext } from '../../src/generators/website-context.js';
|
|
13
14
|
import type { WebsiteStrategyDocument } from '../../src/types/website-strategy.js';
|
|
14
15
|
|
|
@@ -132,6 +133,39 @@ describe('generateWebsiteFooter', () => {
|
|
|
132
133
|
expect(footer).toContain('Resources');
|
|
133
134
|
expect(footer).toContain('Legal');
|
|
134
135
|
});
|
|
136
|
+
|
|
137
|
+
it('all default footer hrefs have corresponding generated pages', () => {
|
|
138
|
+
const footer = generateWebsiteFooter('deploy-ai', contextNoLogo);
|
|
139
|
+
const generatedFiles = getWebsiteProjectFiles('deploy-ai');
|
|
140
|
+
|
|
141
|
+
// Extract all href values from the default footer
|
|
142
|
+
const hrefPattern = /href:\s*'([^']+)'/g;
|
|
143
|
+
let match;
|
|
144
|
+
const hrefs: string[] = [];
|
|
145
|
+
while ((match = hrefPattern.exec(footer)) !== null) {
|
|
146
|
+
hrefs.push(match[1]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Build set of generated page routes from file paths
|
|
150
|
+
const pageRoutes = new Set<string>();
|
|
151
|
+
for (const file of generatedFiles) {
|
|
152
|
+
const pageMatch = file.match(/^src\/app(.*)\/page\.tsx$/);
|
|
153
|
+
if (pageMatch) {
|
|
154
|
+
pageRoutes.add(pageMatch[1] || '/');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Also add root route
|
|
158
|
+
if (generatedFiles.includes('src/app/page.tsx')) {
|
|
159
|
+
pageRoutes.add('/');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Every non-anchor internal href should correspond to a generated page
|
|
163
|
+
for (const href of hrefs) {
|
|
164
|
+
if (href.startsWith('/#')) continue; // Anchor links are fine
|
|
165
|
+
if (!href.startsWith('/')) continue; // External links are fine
|
|
166
|
+
expect(pageRoutes.has(href)).toBe(true);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
135
169
|
});
|
|
136
170
|
|
|
137
171
|
describe('generateWebsiteNavigation', () => {
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Website Content Generator tests — pricing guard, evidence detection,
|
|
3
|
+
* doc path extraction, and per-field AI fallback.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
7
|
+
import { hasPricingEvidence } from '../../src/generators/website-content-ai.js';
|
|
8
|
+
import {
|
|
9
|
+
extractDocPathsFromText,
|
|
10
|
+
validateAndFilterDocPaths,
|
|
11
|
+
validateWebsiteContext,
|
|
12
|
+
} from '../../src/generators/website-context.js';
|
|
13
|
+
import type { WebsiteContentContext } from '../../src/generators/website-context.js';
|
|
14
|
+
|
|
15
|
+
describe('website-content-ai', () => {
|
|
16
|
+
describe('hasPricingEvidence', () => {
|
|
17
|
+
it('should detect dollar amounts in docs', () => {
|
|
18
|
+
expect(hasPricingEvidence('Plans start at $29/mo', '')).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should detect euro amounts', () => {
|
|
22
|
+
expect(hasPricingEvidence('Starting from €19/month', '')).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should detect pricing keywords', () => {
|
|
26
|
+
expect(hasPricingEvidence('Our pricing model includes three tiers', '')).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should detect subscription keywords', () => {
|
|
30
|
+
expect(hasPricingEvidence('Monthly subscription available', '')).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should detect free plan/tier', () => {
|
|
34
|
+
expect(hasPricingEvidence('We offer a free tier for developers', '')).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should detect evidence in specification', () => {
|
|
38
|
+
expect(hasPricingEvidence('', 'Pricing plan: $49/mo')).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return false for docs without pricing info', () => {
|
|
42
|
+
expect(hasPricingEvidence(
|
|
43
|
+
'Our product helps you build amazing software with AI.',
|
|
44
|
+
'A task management tool for teams.',
|
|
45
|
+
)).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return false for empty inputs', () => {
|
|
49
|
+
expect(hasPricingEvidence('', '')).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('pricing guard edge cases', () => {
|
|
54
|
+
it('should detect /yr pattern', () => {
|
|
55
|
+
expect(hasPricingEvidence('$299/yr for annual plans', '')).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should detect per month even without dollar sign', () => {
|
|
59
|
+
// Reason: "per month" is a strong pricing signal regardless of currency symbol
|
|
60
|
+
expect(hasPricingEvidence('10 per month for basic plan', '')).toBe(true);
|
|
61
|
+
expect(hasPricingEvidence('$10 per month for basic plan', '')).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should detect USD pattern', () => {
|
|
65
|
+
expect(hasPricingEvidence('USD 29 per month', '')).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('extractDocPathsFromText', () => {
|
|
70
|
+
it('should extract macOS paths with spaces (quoted)', () => {
|
|
71
|
+
const text = `Build a website for Gateco using '/Users/a b/Gateco/pricing.md'`;
|
|
72
|
+
const paths = extractDocPathsFromText(text);
|
|
73
|
+
expect(paths).toContain('/Users/a b/Gateco/pricing.md');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should extract macOS paths without quotes', () => {
|
|
77
|
+
const text = 'Build a website using /Users/me/Gateco/spec.md please';
|
|
78
|
+
const paths = extractDocPathsFromText(text);
|
|
79
|
+
expect(paths).toContain('/Users/me/Gateco/spec.md');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should extract Windows paths with backslashes (quoted)', () => {
|
|
83
|
+
const text = `Use "C:\\Users\\me\\Gateco\\pricing.md" for content`;
|
|
84
|
+
const paths = extractDocPathsFromText(text);
|
|
85
|
+
expect(paths).toContain('C:\\Users\\me\\Gateco\\pricing.md');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should extract Windows paths with forward slashes', () => {
|
|
89
|
+
const text = `Docs at 'D:/docs/spec.md'`;
|
|
90
|
+
const paths = extractDocPathsFromText(text);
|
|
91
|
+
expect(paths).toContain('D:/docs/spec.md');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should extract multiple paths from mixed text', () => {
|
|
95
|
+
const text = `Build with '/Users/me/spec.md' and "/Users/me/pricing.md"`;
|
|
96
|
+
const paths = extractDocPathsFromText(text);
|
|
97
|
+
expect(paths).toContain('/Users/me/spec.md');
|
|
98
|
+
expect(paths).toContain('/Users/me/pricing.md');
|
|
99
|
+
expect(paths).toHaveLength(2);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should deduplicate paths', () => {
|
|
103
|
+
const text = `Use '/Users/me/spec.md' and '/Users/me/spec.md' twice`;
|
|
104
|
+
const paths = extractDocPathsFromText(text);
|
|
105
|
+
expect(paths).toHaveLength(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should accept .mdx and .txt extensions', () => {
|
|
109
|
+
const text = `Read '/docs/spec.mdx' and '/docs/notes.txt'`;
|
|
110
|
+
const paths = extractDocPathsFromText(text);
|
|
111
|
+
expect(paths).toContain('/docs/spec.mdx');
|
|
112
|
+
expect(paths).toContain('/docs/notes.txt');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should reject .pdf and .docx extensions', () => {
|
|
116
|
+
const text = `Read '/docs/spec.pdf' and '/docs/notes.docx'`;
|
|
117
|
+
const paths = extractDocPathsFromText(text);
|
|
118
|
+
expect(paths).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should return empty for text without paths', () => {
|
|
122
|
+
const text = 'Build a website for my project';
|
|
123
|
+
const paths = extractDocPathsFromText(text);
|
|
124
|
+
expect(paths).toHaveLength(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle paths at end of string', () => {
|
|
128
|
+
const text = 'Build from /Users/me/spec.md';
|
|
129
|
+
const paths = extractDocPathsFromText(text);
|
|
130
|
+
expect(paths).toContain('/Users/me/spec.md');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('validateAndFilterDocPaths', () => {
|
|
135
|
+
it('should reject unsupported extensions with log', async () => {
|
|
136
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
137
|
+
const result = await validateAndFilterDocPaths(['/tmp/doc.pdf']);
|
|
138
|
+
expect(result).toHaveLength(0);
|
|
139
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
140
|
+
expect.stringContaining('unsupported format'),
|
|
141
|
+
);
|
|
142
|
+
logSpy.mockRestore();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should skip non-existent files with log', async () => {
|
|
146
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
147
|
+
const result = await validateAndFilterDocPaths(['/tmp/nonexistent-abc-123.md']);
|
|
148
|
+
expect(result).toHaveLength(0);
|
|
149
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
150
|
+
expect.stringContaining('file not found'),
|
|
151
|
+
);
|
|
152
|
+
logSpy.mockRestore();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should accept allowed extensions for existing files', async () => {
|
|
156
|
+
// Reason: uses node:fs/promises, so we mock stat
|
|
157
|
+
const { promises: fsp } = await import('node:fs');
|
|
158
|
+
const statSpy = vi.spyOn(fsp, 'stat').mockResolvedValue({ size: 1024 } as any);
|
|
159
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
160
|
+
|
|
161
|
+
const result = await validateAndFilterDocPaths(['/tmp/test.md']);
|
|
162
|
+
expect(result).toEqual(['/tmp/test.md']);
|
|
163
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
164
|
+
expect.stringContaining('[doc-ingest] Read:'),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
statSpy.mockRestore();
|
|
168
|
+
logSpy.mockRestore();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should skip oversized files', async () => {
|
|
172
|
+
const { promises: fsp } = await import('node:fs');
|
|
173
|
+
const statSpy = vi.spyOn(fsp, 'stat').mockResolvedValue({
|
|
174
|
+
size: 3 * 1024 * 1024, // 3MB > 2MB limit
|
|
175
|
+
} as any);
|
|
176
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
177
|
+
|
|
178
|
+
const result = await validateAndFilterDocPaths(['/tmp/huge.md']);
|
|
179
|
+
expect(result).toHaveLength(0);
|
|
180
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
181
|
+
expect.stringContaining('exceeds 2MB limit'),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
statSpy.mockRestore();
|
|
185
|
+
logSpy.mockRestore();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('per-field AI fallback', () => {
|
|
190
|
+
it('should treat pricing: [] as missing (empty array)', () => {
|
|
191
|
+
// Reason: the fix changes `!context.pricing` to `context.pricing == null || context.pricing.length === 0`
|
|
192
|
+
const emptyPricing: any[] = [];
|
|
193
|
+
const isMissing = emptyPricing == null || emptyPricing.length === 0;
|
|
194
|
+
expect(isMissing).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should treat pricing: undefined as missing', () => {
|
|
198
|
+
const undefinedPricing = undefined;
|
|
199
|
+
const isMissing = undefinedPricing == null || (undefinedPricing as any)?.length === 0;
|
|
200
|
+
expect(isMissing).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should not treat populated pricing as missing', () => {
|
|
204
|
+
const pricing = [{ name: 'Pro', price: '$29', description: '', features: [], cta: 'Buy' }];
|
|
205
|
+
const isMissing = pricing == null || pricing.length === 0;
|
|
206
|
+
expect(isMissing).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should trigger fallback when only pricing is missing', () => {
|
|
210
|
+
// Reason: verifies per-field logic — tagline exists but pricing is empty
|
|
211
|
+
const context = {
|
|
212
|
+
tagline: 'Existing tagline',
|
|
213
|
+
description: 'Existing description',
|
|
214
|
+
features: [{ title: 'A', description: 'B' }],
|
|
215
|
+
pricing: [] as any[],
|
|
216
|
+
};
|
|
217
|
+
const pricingMissing = context.pricing == null || context.pricing.length === 0;
|
|
218
|
+
const hasMissingContent = !context.tagline || !context.description || context.features.length === 0 || pricingMissing;
|
|
219
|
+
expect(hasMissingContent).toBe(true);
|
|
220
|
+
expect(pricingMissing).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should not trigger fallback when all fields are present', () => {
|
|
224
|
+
const context = {
|
|
225
|
+
tagline: 'Tag',
|
|
226
|
+
description: 'Desc',
|
|
227
|
+
features: [{ title: 'A', description: 'B' }],
|
|
228
|
+
pricing: [{ name: 'Free', price: '$0', description: '', features: [], cta: 'Start' }],
|
|
229
|
+
};
|
|
230
|
+
const pricingMissing = context.pricing == null || context.pricing.length === 0;
|
|
231
|
+
const hasMissingContent = !context.tagline || !context.description || context.features.length === 0 || pricingMissing;
|
|
232
|
+
expect(hasMissingContent).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('validator provenance-based pricing check', () => {
|
|
237
|
+
it('should NOT warn about default pricing when source is docs', () => {
|
|
238
|
+
const context: WebsiteContentContext = {
|
|
239
|
+
productName: 'Gateco',
|
|
240
|
+
features: [{ title: 'Auth', description: 'Auth feature' }],
|
|
241
|
+
rawDocs: '# Gateco\nSome docs content here for minimum length requirement...',
|
|
242
|
+
pricing: [
|
|
243
|
+
{ name: 'Free', price: 'Free', description: '', features: [], cta: 'Get started' },
|
|
244
|
+
{ name: 'Pro', price: '$99', description: '', features: [], cta: 'Start free trial' },
|
|
245
|
+
{ name: 'Enterprise', price: 'Custom', description: '', features: [], cta: 'Contact sales' },
|
|
246
|
+
],
|
|
247
|
+
pricingDiagnostics: {
|
|
248
|
+
charsScanned: 5000,
|
|
249
|
+
foundPricingHeader: true,
|
|
250
|
+
extractionMethod: 'known_plan_names',
|
|
251
|
+
extractedTiers: [
|
|
252
|
+
{ name: 'Free', price: 'Free' },
|
|
253
|
+
{ name: 'Pro', price: '$99' },
|
|
254
|
+
{ name: 'Enterprise', price: 'Custom' },
|
|
255
|
+
],
|
|
256
|
+
source: 'docs',
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
const result = validateWebsiteContext(context, 'Gateco');
|
|
260
|
+
const defaultWarning = result.warnings.find((w) => w.includes('default values'));
|
|
261
|
+
expect(defaultWarning).toBeUndefined();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should NOT warn about default pricing when source is ai', () => {
|
|
265
|
+
const context: WebsiteContentContext = {
|
|
266
|
+
productName: 'TestApp',
|
|
267
|
+
features: [{ title: 'Feature', description: 'Desc' }],
|
|
268
|
+
rawDocs: '# TestApp\nSome docs content here for minimum length...',
|
|
269
|
+
pricing: [
|
|
270
|
+
{ name: 'Starter', price: '$0', description: '', features: [], cta: 'Get started' },
|
|
271
|
+
{ name: 'Pro', price: '$29', description: '', features: [], cta: 'Start' },
|
|
272
|
+
{ name: 'Enterprise', price: 'Custom', description: '', features: [], cta: 'Contact' },
|
|
273
|
+
],
|
|
274
|
+
pricingDiagnostics: {
|
|
275
|
+
charsScanned: 3000,
|
|
276
|
+
foundPricingHeader: false,
|
|
277
|
+
extractionMethod: 'ai',
|
|
278
|
+
extractedTiers: [
|
|
279
|
+
{ name: 'Starter', price: '$0' },
|
|
280
|
+
{ name: 'Pro', price: '$29' },
|
|
281
|
+
{ name: 'Enterprise', price: 'Custom' },
|
|
282
|
+
],
|
|
283
|
+
source: 'ai',
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
const result = validateWebsiteContext(context, 'TestApp');
|
|
287
|
+
const defaultWarning = result.warnings.find((w) => w.includes('default values'));
|
|
288
|
+
expect(defaultWarning).toBeUndefined();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should warn about default pricing when no diagnostics present', () => {
|
|
292
|
+
const context: WebsiteContentContext = {
|
|
293
|
+
productName: 'TestApp',
|
|
294
|
+
features: [{ title: 'Feature', description: 'Desc' }],
|
|
295
|
+
rawDocs: '# TestApp\nSome docs content here for minimum length...',
|
|
296
|
+
pricing: [
|
|
297
|
+
{ name: 'Starter', price: '$0', description: '', features: [], cta: 'Get started' },
|
|
298
|
+
{ name: 'Pro', price: '$29', description: '', features: [], cta: 'Start' },
|
|
299
|
+
{ name: 'Enterprise', price: 'Custom', description: '', features: [], cta: 'Contact' },
|
|
300
|
+
],
|
|
301
|
+
// No pricingDiagnostics — simulates legacy or missing provenance
|
|
302
|
+
};
|
|
303
|
+
const result = validateWebsiteContext(context, 'TestApp');
|
|
304
|
+
const defaultWarning = result.warnings.find((w) => w.includes('default values'));
|
|
305
|
+
expect(defaultWarning).toBeDefined();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|