@yasserkhanorg/e2e-agents 0.3.2
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/LICENSE +168 -0
- package/README.md +620 -0
- package/dist/agent/analysis.d.ts +62 -0
- package/dist/agent/analysis.d.ts.map +1 -0
- package/dist/agent/analysis.js +292 -0
- package/dist/agent/blast_radius.d.ts +4 -0
- package/dist/agent/blast_radius.d.ts.map +1 -0
- package/dist/agent/blast_radius.js +37 -0
- package/dist/agent/cache_utils.d.ts +38 -0
- package/dist/agent/cache_utils.d.ts.map +1 -0
- package/dist/agent/cache_utils.js +67 -0
- package/dist/agent/config.d.ts +148 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +640 -0
- package/dist/agent/dependency_graph.d.ts +14 -0
- package/dist/agent/dependency_graph.d.ts.map +1 -0
- package/dist/agent/dependency_graph.js +227 -0
- package/dist/agent/feedback.d.ts +55 -0
- package/dist/agent/feedback.d.ts.map +1 -0
- package/dist/agent/feedback.js +257 -0
- package/dist/agent/flags.d.ts +23 -0
- package/dist/agent/flags.d.ts.map +1 -0
- package/dist/agent/flags.js +171 -0
- package/dist/agent/flow_catalog.d.ts +25 -0
- package/dist/agent/flow_catalog.d.ts.map +1 -0
- package/dist/agent/flow_catalog.js +106 -0
- package/dist/agent/flow_mapping.d.ts +10 -0
- package/dist/agent/flow_mapping.d.ts.map +1 -0
- package/dist/agent/flow_mapping.js +84 -0
- package/dist/agent/framework.d.ts +13 -0
- package/dist/agent/framework.d.ts.map +1 -0
- package/dist/agent/framework.js +149 -0
- package/dist/agent/gap_suggestions.d.ts +14 -0
- package/dist/agent/gap_suggestions.d.ts.map +1 -0
- package/dist/agent/gap_suggestions.js +101 -0
- package/dist/agent/generator.d.ts +10 -0
- package/dist/agent/generator.d.ts.map +1 -0
- package/dist/agent/generator.js +115 -0
- package/dist/agent/git.d.ts +11 -0
- package/dist/agent/git.d.ts.map +1 -0
- package/dist/agent/git.js +90 -0
- package/dist/agent/handoff.d.ts +22 -0
- package/dist/agent/handoff.d.ts.map +1 -0
- package/dist/agent/handoff.js +180 -0
- package/dist/agent/impact-analyzer.d.ts +114 -0
- package/dist/agent/impact-analyzer.d.ts.map +1 -0
- package/dist/agent/impact-analyzer.js +557 -0
- package/dist/agent/index.d.ts +21 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +38 -0
- package/dist/agent/model-router.d.ts +57 -0
- package/dist/agent/model-router.d.ts.map +1 -0
- package/dist/agent/model-router.js +154 -0
- package/dist/agent/operational_insights.d.ts +41 -0
- package/dist/agent/operational_insights.d.ts.map +1 -0
- package/dist/agent/operational_insights.js +126 -0
- package/dist/agent/pipeline.d.ts +23 -0
- package/dist/agent/pipeline.d.ts.map +1 -0
- package/dist/agent/pipeline.js +609 -0
- package/dist/agent/plan.d.ts +91 -0
- package/dist/agent/plan.d.ts.map +1 -0
- package/dist/agent/plan.js +331 -0
- package/dist/agent/playwright_report.d.ts +8 -0
- package/dist/agent/playwright_report.d.ts.map +1 -0
- package/dist/agent/playwright_report.js +126 -0
- package/dist/agent/report-generator.d.ts +24 -0
- package/dist/agent/report-generator.d.ts.map +1 -0
- package/dist/agent/report-generator.js +250 -0
- package/dist/agent/report.d.ts +81 -0
- package/dist/agent/report.d.ts.map +1 -0
- package/dist/agent/report.js +147 -0
- package/dist/agent/runner.d.ts +7 -0
- package/dist/agent/runner.d.ts.map +1 -0
- package/dist/agent/runner.js +576 -0
- package/dist/agent/selectors.d.ts +10 -0
- package/dist/agent/selectors.d.ts.map +1 -0
- package/dist/agent/selectors.js +75 -0
- package/dist/agent/spec-bridge.d.ts +101 -0
- package/dist/agent/spec-bridge.d.ts.map +1 -0
- package/dist/agent/spec-bridge.js +273 -0
- package/dist/agent/spec-builder.d.ts +102 -0
- package/dist/agent/spec-builder.d.ts.map +1 -0
- package/dist/agent/spec-builder.js +273 -0
- package/dist/agent/subsystem_risk.d.ts +23 -0
- package/dist/agent/subsystem_risk.d.ts.map +1 -0
- package/dist/agent/subsystem_risk.js +207 -0
- package/dist/agent/telemetry.d.ts +84 -0
- package/dist/agent/telemetry.d.ts.map +1 -0
- package/dist/agent/telemetry.js +220 -0
- package/dist/agent/test_path.d.ts +2 -0
- package/dist/agent/test_path.d.ts.map +1 -0
- package/dist/agent/test_path.js +23 -0
- package/dist/agent/tests.d.ts +18 -0
- package/dist/agent/tests.d.ts.map +1 -0
- package/dist/agent/tests.js +106 -0
- package/dist/agent/traceability.d.ts +22 -0
- package/dist/agent/traceability.d.ts.map +1 -0
- package/dist/agent/traceability.js +183 -0
- package/dist/agent/traceability_capture.d.ts +18 -0
- package/dist/agent/traceability_capture.d.ts.map +1 -0
- package/dist/agent/traceability_capture.js +313 -0
- package/dist/agent/traceability_ingest.d.ts +21 -0
- package/dist/agent/traceability_ingest.d.ts.map +1 -0
- package/dist/agent/traceability_ingest.js +237 -0
- package/dist/agent/utils.d.ts +13 -0
- package/dist/agent/utils.d.ts.map +1 -0
- package/dist/agent/utils.js +152 -0
- package/dist/agent/validators/selector-validator.d.ts +74 -0
- package/dist/agent/validators/selector-validator.d.ts.map +1 -0
- package/dist/agent/validators/selector-validator.js +165 -0
- package/dist/anthropic_provider.d.ts +65 -0
- package/dist/anthropic_provider.d.ts.map +1 -0
- package/dist/anthropic_provider.js +332 -0
- package/dist/api.d.ts +48 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +113 -0
- package/dist/base_provider.d.ts +53 -0
- package/dist/base_provider.d.ts.map +1 -0
- package/dist/base_provider.js +81 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +843 -0
- package/dist/custom_provider.d.ts +20 -0
- package/dist/custom_provider.d.ts.map +1 -0
- package/dist/custom_provider.js +276 -0
- package/dist/e2e-test-gen/index.d.ts +51 -0
- package/dist/e2e-test-gen/index.d.ts.map +1 -0
- package/dist/e2e-test-gen/index.js +57 -0
- package/dist/e2e-test-gen/spec_parser.d.ts +142 -0
- package/dist/e2e-test-gen/spec_parser.d.ts.map +1 -0
- package/dist/e2e-test-gen/spec_parser.js +786 -0
- package/dist/e2e-test-gen/types.d.ts +185 -0
- package/dist/e2e-test-gen/types.d.ts.map +1 -0
- package/dist/e2e-test-gen/types.js +4 -0
- package/dist/esm/agent/analysis.js +287 -0
- package/dist/esm/agent/blast_radius.js +34 -0
- package/dist/esm/agent/cache_utils.js +63 -0
- package/dist/esm/agent/config.js +637 -0
- package/dist/esm/agent/dependency_graph.js +224 -0
- package/dist/esm/agent/feedback.js +253 -0
- package/dist/esm/agent/flags.js +160 -0
- package/dist/esm/agent/flow_catalog.js +103 -0
- package/dist/esm/agent/flow_mapping.js +81 -0
- package/dist/esm/agent/framework.js +145 -0
- package/dist/esm/agent/gap_suggestions.js +98 -0
- package/dist/esm/agent/generator.js +112 -0
- package/dist/esm/agent/git.js +87 -0
- package/dist/esm/agent/handoff.js +177 -0
- package/dist/esm/agent/impact-analyzer.js +548 -0
- package/dist/esm/agent/index.js +22 -0
- package/dist/esm/agent/model-router.js +150 -0
- package/dist/esm/agent/operational_insights.js +123 -0
- package/dist/esm/agent/pipeline.js +605 -0
- package/dist/esm/agent/plan.js +324 -0
- package/dist/esm/agent/playwright_report.js +123 -0
- package/dist/esm/agent/report-generator.js +247 -0
- package/dist/esm/agent/report.js +144 -0
- package/dist/esm/agent/runner.js +572 -0
- package/dist/esm/agent/selectors.js +71 -0
- package/dist/esm/agent/spec-bridge.js +267 -0
- package/dist/esm/agent/spec-builder.js +267 -0
- package/dist/esm/agent/subsystem_risk.js +204 -0
- package/dist/esm/agent/telemetry.js +216 -0
- package/dist/esm/agent/test_path.js +20 -0
- package/dist/esm/agent/tests.js +101 -0
- package/dist/esm/agent/traceability.js +180 -0
- package/dist/esm/agent/traceability_capture.js +310 -0
- package/dist/esm/agent/traceability_ingest.js +234 -0
- package/dist/esm/agent/utils.js +138 -0
- package/dist/esm/agent/validators/selector-validator.js +160 -0
- package/dist/esm/anthropic_provider.js +324 -0
- package/dist/esm/api.js +105 -0
- package/dist/esm/base_provider.js +77 -0
- package/dist/esm/cli.js +841 -0
- package/dist/esm/custom_provider.js +272 -0
- package/dist/esm/e2e-test-gen/index.js +50 -0
- package/dist/esm/e2e-test-gen/spec_parser.js +782 -0
- package/dist/esm/e2e-test-gen/types.js +3 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/logger.js +89 -0
- package/dist/esm/mcp-server.js +465 -0
- package/dist/esm/ollama_provider.js +300 -0
- package/dist/esm/openai_provider.js +242 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/plan-and-test-constants.js +126 -0
- package/dist/esm/provider_factory.js +336 -0
- package/dist/esm/provider_interface.js +23 -0
- package/dist/esm/provider_utils.js +96 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +93 -0
- package/dist/mcp-server.d.ts +35 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +469 -0
- package/dist/ollama_provider.d.ts +65 -0
- package/dist/ollama_provider.d.ts.map +1 -0
- package/dist/ollama_provider.js +308 -0
- package/dist/openai_provider.d.ts +23 -0
- package/dist/openai_provider.d.ts.map +1 -0
- package/dist/openai_provider.js +250 -0
- package/dist/plan-and-test-constants.d.ts +110 -0
- package/dist/plan-and-test-constants.d.ts.map +1 -0
- package/dist/plan-and-test-constants.js +132 -0
- package/dist/provider_factory.d.ts +99 -0
- package/dist/provider_factory.d.ts.map +1 -0
- package/dist/provider_factory.js +341 -0
- package/dist/provider_interface.d.ts +358 -0
- package/dist/provider_interface.d.ts.map +1 -0
- package/dist/provider_interface.js +28 -0
- package/dist/provider_utils.d.ts +39 -0
- package/dist/provider_utils.d.ts.map +1 -0
- package/dist/provider_utils.js +103 -0
- package/package.json +101 -0
- package/schemas/gap.schema.json +18 -0
- package/schemas/impact.schema.json +418 -0
- package/schemas/plan.schema.json +285 -0
- package/schemas/subsystem-risk-map.schema.json +62 -0
- package/schemas/traceability-input.schema.json +122 -0
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
import { readFileSync, existsSync } from 'fs';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
/**
|
|
7
|
+
* Specification Parser
|
|
8
|
+
*
|
|
9
|
+
* Parses user-provided specification documents to guide autonomous testing.
|
|
10
|
+
* Supports multiple formats:
|
|
11
|
+
* - Markdown (.md) with embedded screenshots and Given-When-Then scenarios
|
|
12
|
+
* - JSON (.json) with structured feature definitions
|
|
13
|
+
* - PDF (.pdf) with text, diagrams, and screenshots extracted via LLM
|
|
14
|
+
* - Plain text focus strings (natural language)
|
|
15
|
+
*
|
|
16
|
+
* Extracts:
|
|
17
|
+
* - Feature name, description, priority
|
|
18
|
+
* - Target URLs to prioritize in crawling
|
|
19
|
+
* - Business scenarios (Given-When-Then)
|
|
20
|
+
* - Acceptance criteria
|
|
21
|
+
* - Reference screenshots for visual comparison
|
|
22
|
+
* - UI mockups and wireframes from PDFs
|
|
23
|
+
*
|
|
24
|
+
* The parsed specifications guide:
|
|
25
|
+
* - Crawler URL prioritization
|
|
26
|
+
* - Test scenario generation
|
|
27
|
+
* - Visual regression comparison
|
|
28
|
+
* - Coverage gap detection
|
|
29
|
+
*/
|
|
30
|
+
export class SpecificationParser {
|
|
31
|
+
constructor(llmProvider, cache) {
|
|
32
|
+
this.llmProvider = llmProvider;
|
|
33
|
+
this.cache = cache;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parse specification from file or string
|
|
37
|
+
*/
|
|
38
|
+
async parse(source, sourceType = 'file') {
|
|
39
|
+
let content;
|
|
40
|
+
let sourcePath;
|
|
41
|
+
if (sourceType === 'file') {
|
|
42
|
+
if (!existsSync(source)) {
|
|
43
|
+
throw new Error(`Specification file not found: ${source}`);
|
|
44
|
+
}
|
|
45
|
+
sourcePath = source;
|
|
46
|
+
// Determine format from extension
|
|
47
|
+
if (source.endsWith('.md')) {
|
|
48
|
+
content = readFileSync(source, 'utf-8');
|
|
49
|
+
return this.parseMarkdown(content, sourcePath);
|
|
50
|
+
}
|
|
51
|
+
else if (source.endsWith('.json')) {
|
|
52
|
+
content = readFileSync(source, 'utf-8');
|
|
53
|
+
return this.parseJSON(content, sourcePath);
|
|
54
|
+
}
|
|
55
|
+
else if (source.endsWith('.pdf')) {
|
|
56
|
+
return this.parsePDF(sourcePath);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
throw new Error(`Unsupported specification format: ${source}. Use .md, .json, or .pdf`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// Plain text focus string - use LLM to interpret
|
|
64
|
+
return this.parseFocusString(source);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Parse Markdown specification
|
|
69
|
+
*
|
|
70
|
+
* Expected format:
|
|
71
|
+
* # Feature: Feature Name
|
|
72
|
+
* **Priority**: High
|
|
73
|
+
* **Target URLs**: /path1, /path2
|
|
74
|
+
*
|
|
75
|
+
* ## Description
|
|
76
|
+
* Feature description...
|
|
77
|
+
*
|
|
78
|
+
* ## Business Scenarios
|
|
79
|
+
* ### Scenario 1: Name
|
|
80
|
+
* - **Given**: Precondition
|
|
81
|
+
* - **When**: Action
|
|
82
|
+
* - **Then**: Expected outcome
|
|
83
|
+
*
|
|
84
|
+
* ## Acceptance Criteria
|
|
85
|
+
* - Criterion 1
|
|
86
|
+
* - Criterion 2
|
|
87
|
+
*
|
|
88
|
+
* ## Screenshots
|
|
89
|
+
* 
|
|
90
|
+
*/
|
|
91
|
+
async parseMarkdown(content, sourcePath) {
|
|
92
|
+
// Dynamic import to handle ESM marked module in CommonJS context
|
|
93
|
+
const { marked } = await import('marked');
|
|
94
|
+
const tokens = marked.lexer(content);
|
|
95
|
+
const specs = [];
|
|
96
|
+
let currentSpec = null;
|
|
97
|
+
let currentSection = null;
|
|
98
|
+
let currentScenario = null;
|
|
99
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
100
|
+
const token = tokens[i];
|
|
101
|
+
if (token.type === 'heading') {
|
|
102
|
+
const headingToken = token;
|
|
103
|
+
const text = headingToken.text;
|
|
104
|
+
if (headingToken.depth === 1) {
|
|
105
|
+
// New feature
|
|
106
|
+
if (currentSpec) {
|
|
107
|
+
specs.push(this.finalizeSpec(currentSpec, sourcePath));
|
|
108
|
+
}
|
|
109
|
+
currentSpec = {
|
|
110
|
+
name: text.replace(/^Feature:\s*/i, '').trim(),
|
|
111
|
+
targetUrls: [],
|
|
112
|
+
scenarios: [],
|
|
113
|
+
screenshots: [],
|
|
114
|
+
acceptanceCriteria: [],
|
|
115
|
+
};
|
|
116
|
+
currentSection = null;
|
|
117
|
+
}
|
|
118
|
+
else if (headingToken.depth === 2) {
|
|
119
|
+
// Section
|
|
120
|
+
currentSection = text.toLowerCase();
|
|
121
|
+
}
|
|
122
|
+
else if (headingToken.depth === 3 && currentSection === 'business scenarios') {
|
|
123
|
+
// New scenario
|
|
124
|
+
if (currentScenario && currentSpec) {
|
|
125
|
+
currentSpec.scenarios.push(this.finalizeScenario(currentScenario));
|
|
126
|
+
}
|
|
127
|
+
currentScenario = {
|
|
128
|
+
name: text.replace(/^Scenario \d+:\s*/i, '').trim(),
|
|
129
|
+
priority: 'should-have',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (token.type === 'paragraph' && currentSpec) {
|
|
134
|
+
const paragraphToken = token;
|
|
135
|
+
const text = paragraphToken.text;
|
|
136
|
+
// Extract metadata from bold markers
|
|
137
|
+
const priorityMatch = text.match(/\*\*Priority\*\*:\s*(\w+)/i);
|
|
138
|
+
if (priorityMatch) {
|
|
139
|
+
currentSpec.priority = priorityMatch[1].toLowerCase();
|
|
140
|
+
}
|
|
141
|
+
const urlsMatch = text.match(/\*\*Target URLs?\*\*:\s*(.+)/i);
|
|
142
|
+
if (urlsMatch) {
|
|
143
|
+
currentSpec.targetUrls = urlsMatch[1]
|
|
144
|
+
.split(',')
|
|
145
|
+
.map((url) => url.trim())
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
}
|
|
148
|
+
// Extract scenario details
|
|
149
|
+
if (currentScenario) {
|
|
150
|
+
const givenMatch = text.match(/\*\*Given\*\*:\s*(.+)/i);
|
|
151
|
+
const whenMatch = text.match(/\*\*When\*\*:\s*(.+)/i);
|
|
152
|
+
const thenMatch = text.match(/\*\*Then\*\*:\s*(.+)/i);
|
|
153
|
+
const priorityMatch = text.match(/\*\*Priority\*\*:\s*(\w+)/i);
|
|
154
|
+
if (givenMatch)
|
|
155
|
+
currentScenario.given = givenMatch[1].trim();
|
|
156
|
+
if (whenMatch)
|
|
157
|
+
currentScenario.when = whenMatch[1].trim();
|
|
158
|
+
if (thenMatch)
|
|
159
|
+
currentScenario.then = thenMatch[1].trim();
|
|
160
|
+
if (priorityMatch) {
|
|
161
|
+
const priority = priorityMatch[1].toLowerCase();
|
|
162
|
+
if (priority.includes('must'))
|
|
163
|
+
currentScenario.priority = 'must-have';
|
|
164
|
+
else if (priority.includes('should'))
|
|
165
|
+
currentScenario.priority = 'should-have';
|
|
166
|
+
else
|
|
167
|
+
currentScenario.priority = 'nice-to-have';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Description section
|
|
171
|
+
if (currentSection === 'description') {
|
|
172
|
+
currentSpec.description = (currentSpec.description || '') + text + ' ';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (token.type === 'list' && currentSpec) {
|
|
176
|
+
const listToken = token;
|
|
177
|
+
if (currentSection === 'acceptance criteria') {
|
|
178
|
+
// Extract acceptance criteria
|
|
179
|
+
for (const item of listToken.items) {
|
|
180
|
+
const itemToken = item;
|
|
181
|
+
currentSpec.acceptanceCriteria.push(itemToken.text);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (currentSection === 'business scenarios' && currentScenario) {
|
|
185
|
+
// Extract Given-When-Then from list items
|
|
186
|
+
for (const item of listToken.items) {
|
|
187
|
+
const itemToken = item;
|
|
188
|
+
const text = itemToken.text;
|
|
189
|
+
const givenMatch = text.match(/\*\*Given\*\*:\s*(.+)/i);
|
|
190
|
+
const whenMatch = text.match(/\*\*When\*\*:\s*(.+)/i);
|
|
191
|
+
const thenMatch = text.match(/\*\*Then\*\*:\s*(.+)/i);
|
|
192
|
+
if (givenMatch)
|
|
193
|
+
currentScenario.given = givenMatch[1].trim();
|
|
194
|
+
if (whenMatch)
|
|
195
|
+
currentScenario.when = whenMatch[1].trim();
|
|
196
|
+
if (thenMatch)
|
|
197
|
+
currentScenario.then = thenMatch[1].trim();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else if (token.type === 'image' && currentSpec && currentSection === 'screenshots') {
|
|
202
|
+
const imageToken = token;
|
|
203
|
+
// Resolve image path relative to spec file
|
|
204
|
+
const imagePath = this.resolveImagePath(imageToken.href, sourcePath);
|
|
205
|
+
currentSpec.screenshots.push({
|
|
206
|
+
path: imagePath,
|
|
207
|
+
description: imageToken.text || 'Screenshot',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Finalize last scenario and spec
|
|
212
|
+
if (currentScenario && currentSpec) {
|
|
213
|
+
currentSpec.scenarios.push(this.finalizeScenario(currentScenario));
|
|
214
|
+
}
|
|
215
|
+
if (currentSpec) {
|
|
216
|
+
specs.push(this.finalizeSpec(currentSpec, sourcePath));
|
|
217
|
+
}
|
|
218
|
+
// Load screenshot data
|
|
219
|
+
await this.loadScreenshots(specs);
|
|
220
|
+
return specs;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Parse JSON specification
|
|
224
|
+
*/
|
|
225
|
+
parseJSON(content, sourcePath) {
|
|
226
|
+
try {
|
|
227
|
+
let data;
|
|
228
|
+
try {
|
|
229
|
+
data = JSON.parse(content);
|
|
230
|
+
}
|
|
231
|
+
catch (parseError) {
|
|
232
|
+
throw new Error(`Invalid JSON syntax: ${parseError.message}`);
|
|
233
|
+
}
|
|
234
|
+
if (!data) {
|
|
235
|
+
throw new Error('JSON is empty or null');
|
|
236
|
+
}
|
|
237
|
+
// Support both single spec and array of specs
|
|
238
|
+
const specsData = Array.isArray(data) ? data : [data];
|
|
239
|
+
if (specsData.length === 0) {
|
|
240
|
+
throw new Error('No specifications found in JSON');
|
|
241
|
+
}
|
|
242
|
+
const specs = specsData.map((specData, index) => {
|
|
243
|
+
// Validate required fields
|
|
244
|
+
if (!specData.name && !specData.feature) {
|
|
245
|
+
throw new Error(`Specification ${index + 1} missing required "name" or "feature" field`);
|
|
246
|
+
}
|
|
247
|
+
if (!specData.scenarios || !Array.isArray(specData.scenarios)) {
|
|
248
|
+
// eslint-disable-next-line no-console
|
|
249
|
+
console.warn(`Specification "${specData.name || specData.feature}" has no scenarios`);
|
|
250
|
+
}
|
|
251
|
+
const scenarios = (specData.scenarios || []).map((s, sIndex) => {
|
|
252
|
+
if (!s.name) {
|
|
253
|
+
throw new Error(`Scenario ${sIndex + 1} in "${specData.name || specData.feature}" missing "name" field`);
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
name: s.name,
|
|
257
|
+
given: s.given || '',
|
|
258
|
+
when: s.when || '',
|
|
259
|
+
then: s.then || '',
|
|
260
|
+
priority: s.priority || 'should-have',
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
const screenshots = (specData.screenshots || []).map((s) => {
|
|
264
|
+
if (typeof s === 'string') {
|
|
265
|
+
return { path: this.resolveImagePath(s, sourcePath), description: 'Screenshot' };
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
path: this.resolveImagePath(s.path, sourcePath),
|
|
269
|
+
description: s.description || 'Screenshot',
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
id: this.generateSpecId(specData.feature || specData.name),
|
|
274
|
+
name: specData.feature || specData.name,
|
|
275
|
+
description: specData.description || '',
|
|
276
|
+
priority: specData.priority || 'medium',
|
|
277
|
+
targetUrls: specData.targetUrls || specData.urls || [],
|
|
278
|
+
scenarios,
|
|
279
|
+
screenshots,
|
|
280
|
+
acceptanceCriteria: specData.acceptanceCriteria || [],
|
|
281
|
+
sourcePath,
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
// Load screenshot data
|
|
285
|
+
this.loadScreenshots(specs);
|
|
286
|
+
return specs;
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
throw new Error(`Failed to parse JSON specification: ${error instanceof Error ? error.message : String(error)}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Parse PDF specification using LLM vision
|
|
294
|
+
*
|
|
295
|
+
* PDFs can contain:
|
|
296
|
+
* - Product requirement documents (PRDs)
|
|
297
|
+
* - Feature specifications with screenshots
|
|
298
|
+
* - UI mockups and wireframes
|
|
299
|
+
* - User flow diagrams
|
|
300
|
+
* - Acceptance criteria and test plans
|
|
301
|
+
*
|
|
302
|
+
* The LLM will extract structured information including:
|
|
303
|
+
* - Feature name and description
|
|
304
|
+
* - Business scenarios
|
|
305
|
+
* - Target URLs (inferred from screenshots/mockups)
|
|
306
|
+
* - Acceptance criteria
|
|
307
|
+
* - Screenshots/mockups for visual comparison
|
|
308
|
+
*/
|
|
309
|
+
async parsePDF(pdfPath) {
|
|
310
|
+
/* eslint-disable no-console */
|
|
311
|
+
// Console output is expected for PDF parsing progress
|
|
312
|
+
console.log(`📄 Parsing PDF specification: ${pdfPath}`);
|
|
313
|
+
// Read PDF file first to calculate hash
|
|
314
|
+
const pdfBuffer = readFileSync(pdfPath);
|
|
315
|
+
const pdfHash = createHash('sha256').update(pdfBuffer).digest('hex');
|
|
316
|
+
// Check if already cached
|
|
317
|
+
if (this.cache && this.cache.isSpecificationCached(pdfPath, pdfHash)) {
|
|
318
|
+
console.log(` ✓ Using cached PDF specification (hash: ${pdfHash.substring(0, 8)}...)`);
|
|
319
|
+
const cachedSpecs = this.cache.getCachedSpecifications(pdfPath, pdfHash);
|
|
320
|
+
console.log(`✅ Loaded ${cachedSpecs.length} feature(s) from cache`);
|
|
321
|
+
// Print scenario count for each feature
|
|
322
|
+
for (const spec of cachedSpecs) {
|
|
323
|
+
const priority = spec.priority === 'critical' ? 'critical' : spec.priority;
|
|
324
|
+
console.log(` ✓ Loaded 1 specifications`);
|
|
325
|
+
console.log(` - ${spec.name} (${priority}): ${spec.scenarios.length} scenarios`);
|
|
326
|
+
}
|
|
327
|
+
return cachedSpecs;
|
|
328
|
+
}
|
|
329
|
+
// Check if provider supports vision (required for PDF parsing)
|
|
330
|
+
if (!this.llmProvider.capabilities.vision) {
|
|
331
|
+
throw new Error('PDF parsing requires a vision-capable LLM provider (e.g., Anthropic Claude). ' +
|
|
332
|
+
'Current provider does not support vision. ' +
|
|
333
|
+
'Use --llm-provider anthropic or --llm-provider hybrid');
|
|
334
|
+
}
|
|
335
|
+
// SECURITY WARNING: PDF will be sent to external LLM
|
|
336
|
+
console.warn('');
|
|
337
|
+
console.warn('⚠️ SECURITY WARNING: PDF Document Transmission ⚠️');
|
|
338
|
+
console.warn('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
339
|
+
console.warn('The PDF specification will be sent to an external LLM provider.');
|
|
340
|
+
console.warn('');
|
|
341
|
+
console.warn('Ensure the PDF does NOT contain:');
|
|
342
|
+
console.warn(' • Internal API keys or credentials');
|
|
343
|
+
console.warn(' • Sensitive architecture details');
|
|
344
|
+
console.warn(' • Production URLs or IP addresses');
|
|
345
|
+
console.warn(' • Personal Identifying Information (PII)');
|
|
346
|
+
console.warn(' • Proprietary business information');
|
|
347
|
+
console.warn('');
|
|
348
|
+
console.warn('The LLM provider may retain this data per their policy.');
|
|
349
|
+
console.warn('For Anthropic: https://www.anthropic.com/legal/privacy');
|
|
350
|
+
console.warn('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
351
|
+
console.warn('');
|
|
352
|
+
// Check if user has consented (via environment variable)
|
|
353
|
+
if (!process.env.AUTONOMOUS_ALLOW_PDF_UPLOAD) {
|
|
354
|
+
console.error('❌ PDF upload blocked for security.');
|
|
355
|
+
console.error('❌ To proceed, set environment variable:');
|
|
356
|
+
console.error('❌ AUTONOMOUS_ALLOW_PDF_UPLOAD=true');
|
|
357
|
+
console.error('❌ Only set this if you have reviewed the PDF for sensitive data.');
|
|
358
|
+
throw new Error('PDF upload requires explicit consent via AUTONOMOUS_ALLOW_PDF_UPLOAD=true');
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
console.log(`📄 Extracting text from PDF... (${(pdfBuffer.length / 1024).toFixed(2)} KB)`);
|
|
362
|
+
// Parse PDF to extract text content (dynamic import with default export)
|
|
363
|
+
const pdfParse = (await import('pdf-parse')).default;
|
|
364
|
+
const pdfData = await pdfParse(pdfBuffer);
|
|
365
|
+
const pdfText = pdfData.text;
|
|
366
|
+
console.log(` ✓ Extracted ${pdfText.length} characters, ${pdfData.numpages} pages`);
|
|
367
|
+
// Use LLM for semantic parsing (text-only, no vision)
|
|
368
|
+
const prompt = `
|
|
369
|
+
You are a test specification parser. Parse this UX/product specification document and extract structured, machine-operable testing information.
|
|
370
|
+
|
|
371
|
+
The document describes features, user flows, permissions, edge cases, and acceptance criteria.
|
|
372
|
+
|
|
373
|
+
Extract the following in JSON format (this schema is optimized for test automation):
|
|
374
|
+
|
|
375
|
+
{
|
|
376
|
+
"features": [
|
|
377
|
+
{
|
|
378
|
+
"name": "Feature Name (e.g., Auto-translation)",
|
|
379
|
+
"scope": "MVP|v1|v2",
|
|
380
|
+
"description": "What this feature does",
|
|
381
|
+
"priority": "critical|high|medium|low",
|
|
382
|
+
|
|
383
|
+
"roles": {
|
|
384
|
+
"role_name": {
|
|
385
|
+
"permissions": ["permission1", "permission2"]
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
"enablement": {
|
|
390
|
+
"location_type": {
|
|
391
|
+
"enabled_by": "who can enable",
|
|
392
|
+
"default_state": "on|off",
|
|
393
|
+
"side_effects": ["system_message", "label_visible"]
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
"scenarios": [
|
|
398
|
+
{
|
|
399
|
+
"name": "Scenario name",
|
|
400
|
+
"given": "Precondition",
|
|
401
|
+
"when": "Action",
|
|
402
|
+
"then": "Expected outcome",
|
|
403
|
+
"priority": "must-have|should-have|nice-to-have"
|
|
404
|
+
}
|
|
405
|
+
],
|
|
406
|
+
|
|
407
|
+
"acceptanceCriteria": [
|
|
408
|
+
"Testable criterion"
|
|
409
|
+
],
|
|
410
|
+
|
|
411
|
+
"stateMachines": {
|
|
412
|
+
"message_lifecycle": {
|
|
413
|
+
"states": ["translating", "translated", "failed"],
|
|
414
|
+
"transitions": ["new_message -> translating", "translating -> translated"]
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
"uiIndicators": {
|
|
419
|
+
"location": "visual_indicator"
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
"edgeCases": [
|
|
423
|
+
"What happens when X"
|
|
424
|
+
],
|
|
425
|
+
|
|
426
|
+
"platformDifferences": {
|
|
427
|
+
"desktop": "behavior",
|
|
428
|
+
"mobile": "behavior"
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
"nonGoals": [
|
|
432
|
+
"Out of scope items"
|
|
433
|
+
]
|
|
434
|
+
}
|
|
435
|
+
]
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
IMPORTANT:
|
|
439
|
+
- Extract ALL features, flows, roles, and permissions
|
|
440
|
+
- Map state machines if described
|
|
441
|
+
- Identify edge cases and failure modes
|
|
442
|
+
- List platform differences
|
|
443
|
+
- Capture non-goals/out-of-scope items
|
|
444
|
+
- DO NOT infer or generate target URLs - feature specs describe behavior, not routes
|
|
445
|
+
- Be precise with permissions and access control
|
|
446
|
+
- For MVP features or P0 features, set priority to "critical"
|
|
447
|
+
- For must-have scenarios in MVP features, set scenario priority to "must-have"
|
|
448
|
+
|
|
449
|
+
Respond with ONLY valid JSON, no markdown formatting.
|
|
450
|
+
|
|
451
|
+
DOCUMENT TEXT:
|
|
452
|
+
${pdfText}
|
|
453
|
+
`.trim();
|
|
454
|
+
// Call LLM with text (no vision)
|
|
455
|
+
// Use higher maxTokens for complex documents (16K for large specs)
|
|
456
|
+
const response = await this.llmProvider.generateText(prompt, {
|
|
457
|
+
maxTokens: 16000,
|
|
458
|
+
temperature: 0.1, // Very low temperature for structured extraction
|
|
459
|
+
});
|
|
460
|
+
// Parse LLM response
|
|
461
|
+
let jsonText = response.text.trim();
|
|
462
|
+
// Remove markdown code blocks if present (handle various formats)
|
|
463
|
+
// Try multiple patterns to be more robust
|
|
464
|
+
const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]+?)\s*```/);
|
|
465
|
+
if (jsonMatch) {
|
|
466
|
+
jsonText = jsonMatch[1].trim();
|
|
467
|
+
}
|
|
468
|
+
else if (jsonText.startsWith('```')) {
|
|
469
|
+
// Fallback: remove all triple backticks
|
|
470
|
+
jsonText = jsonText
|
|
471
|
+
.replace(/```[^\n]*\n?/g, '')
|
|
472
|
+
.replace(/```\s*$/g, '')
|
|
473
|
+
.trim();
|
|
474
|
+
}
|
|
475
|
+
let data;
|
|
476
|
+
try {
|
|
477
|
+
data = JSON.parse(jsonText);
|
|
478
|
+
}
|
|
479
|
+
catch (parseError) {
|
|
480
|
+
// Log the problematic JSON for debugging
|
|
481
|
+
console.error('Failed to parse LLM response as JSON');
|
|
482
|
+
console.error('First 500 chars:', jsonText.substring(0, 500));
|
|
483
|
+
console.error('Last 500 chars:', jsonText.substring(Math.max(0, jsonText.length - 500)));
|
|
484
|
+
// Try to repair common JSON issues
|
|
485
|
+
console.log('Attempting to repair JSON...');
|
|
486
|
+
let repairedJson = jsonText;
|
|
487
|
+
// Common fixes:
|
|
488
|
+
// 1. Fix unquoted property names (e.g., {name: "value"} -> {"name": "value"})
|
|
489
|
+
repairedJson = repairedJson.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
|
|
490
|
+
// 2. Fix single quotes to double quotes
|
|
491
|
+
repairedJson = repairedJson.replace(/'/g, '"');
|
|
492
|
+
// 3. Remove trailing commas
|
|
493
|
+
repairedJson = repairedJson.replace(/,(\s*[}\]])/g, '$1');
|
|
494
|
+
// 4. Fix missing quotes around string values (this is tricky, skip for now)
|
|
495
|
+
try {
|
|
496
|
+
data = JSON.parse(repairedJson);
|
|
497
|
+
console.log('✓ JSON repair successful!');
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
// If repair failed, ask LLM to fix it
|
|
501
|
+
console.log('JSON repair failed, requesting LLM to fix the JSON...');
|
|
502
|
+
const fixPrompt = `
|
|
503
|
+
The following JSON is malformed. Please fix it and return ONLY valid JSON with no markdown formatting:
|
|
504
|
+
|
|
505
|
+
${jsonText}
|
|
506
|
+
|
|
507
|
+
Error: ${parseError instanceof Error ? parseError.message : String(parseError)}
|
|
508
|
+
|
|
509
|
+
Return the corrected JSON:`.trim();
|
|
510
|
+
const fixResponse = await this.llmProvider.generateText(fixPrompt, {
|
|
511
|
+
maxTokens: 16000,
|
|
512
|
+
temperature: 0,
|
|
513
|
+
});
|
|
514
|
+
let fixedJson = fixResponse.text.trim();
|
|
515
|
+
// Remove markdown code blocks again
|
|
516
|
+
const fixedMatch = fixedJson.match(/```(?:json)?\s*([\s\S]+?)\s*```/);
|
|
517
|
+
if (fixedMatch) {
|
|
518
|
+
fixedJson = fixedMatch[1].trim();
|
|
519
|
+
}
|
|
520
|
+
else if (fixedJson.startsWith('```')) {
|
|
521
|
+
fixedJson = fixedJson
|
|
522
|
+
.replace(/```[^\n]*\n?/g, '')
|
|
523
|
+
.replace(/```\s*$/g, '')
|
|
524
|
+
.trim();
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
data = JSON.parse(fixedJson);
|
|
528
|
+
console.log('✓ LLM successfully fixed the JSON!');
|
|
529
|
+
}
|
|
530
|
+
catch (finalError) {
|
|
531
|
+
console.error('Failed to parse even after LLM repair');
|
|
532
|
+
console.error('Original error:', parseError);
|
|
533
|
+
console.error('Repair error:', finalError);
|
|
534
|
+
throw parseError; // Throw original error
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (!data.features || !Array.isArray(data.features)) {
|
|
539
|
+
throw new Error('Invalid PDF parse response: missing features array');
|
|
540
|
+
}
|
|
541
|
+
console.log(`✅ Extracted ${data.features.length} feature(s) from PDF`);
|
|
542
|
+
// Convert to FeatureSpecification format
|
|
543
|
+
const specs = data.features.map((feature) => {
|
|
544
|
+
const scenarios = (feature.scenarios || []).map((s) => ({
|
|
545
|
+
name: s.name,
|
|
546
|
+
given: s.given || '',
|
|
547
|
+
when: s.when || '',
|
|
548
|
+
then: s.then || '',
|
|
549
|
+
priority: s.priority || 'should-have',
|
|
550
|
+
}));
|
|
551
|
+
// Store screenshot metadata (page numbers for extraction)
|
|
552
|
+
const screenshots = (feature.screenshots || []).map((s) => ({
|
|
553
|
+
path: `${pdfPath}#page=${s.pageNumber}`,
|
|
554
|
+
description: s.description || `Page ${s.pageNumber}`,
|
|
555
|
+
pageNumber: s.pageNumber,
|
|
556
|
+
}));
|
|
557
|
+
const spec = {
|
|
558
|
+
id: this.generateSpecId(feature.name),
|
|
559
|
+
name: feature.name,
|
|
560
|
+
description: feature.description || '',
|
|
561
|
+
priority: feature.priority || 'medium',
|
|
562
|
+
targetUrls: feature.targetUrls || [],
|
|
563
|
+
scenarios,
|
|
564
|
+
screenshots,
|
|
565
|
+
acceptanceCriteria: feature.acceptanceCriteria || [],
|
|
566
|
+
sourcePath: pdfPath,
|
|
567
|
+
sourceHash: pdfHash,
|
|
568
|
+
metadata: {
|
|
569
|
+
uiElements: feature.uiElements || [],
|
|
570
|
+
userFlows: feature.userFlows || [],
|
|
571
|
+
extractedFrom: 'pdf',
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
return spec;
|
|
575
|
+
});
|
|
576
|
+
// Save to cache if knowledge base is available
|
|
577
|
+
if (this.cache) {
|
|
578
|
+
for (const spec of specs) {
|
|
579
|
+
this.cache.saveSpecification(spec);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/* eslint-enable no-console */
|
|
583
|
+
return specs;
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
if (error instanceof Error && error.message.includes('vision')) {
|
|
587
|
+
throw error; // Re-throw vision capability errors
|
|
588
|
+
}
|
|
589
|
+
throw new Error(`Failed to parse PDF specification: ${error instanceof Error ? error.message : String(error)}. ` +
|
|
590
|
+
'Ensure the PDF contains readable text and images. ' +
|
|
591
|
+
'For scanned PDFs, ensure they have been OCR processed.');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Parse natural language focus string using LLM
|
|
596
|
+
*/
|
|
597
|
+
async parseFocusString(focusString) {
|
|
598
|
+
const prompt = `
|
|
599
|
+
You are a test specification generator. Given a natural language focus string, extract:
|
|
600
|
+
1. Feature name (infer from the focus string)
|
|
601
|
+
2. Target URLs (guess likely URLs based on feature names)
|
|
602
|
+
3. Business scenarios (generate 2-3 test scenarios)
|
|
603
|
+
4. Priority (infer from words like "critical", "thoroughly", etc.)
|
|
604
|
+
|
|
605
|
+
Focus string: "${focusString}"
|
|
606
|
+
|
|
607
|
+
Respond with valid JSON in this format:
|
|
608
|
+
{
|
|
609
|
+
"feature": "Feature Name",
|
|
610
|
+
"priority": "high|medium|low",
|
|
611
|
+
"targetUrls": ["/url1", "/url2"],
|
|
612
|
+
"scenarios": [
|
|
613
|
+
{
|
|
614
|
+
"name": "Scenario name",
|
|
615
|
+
"given": "Precondition",
|
|
616
|
+
"when": "Action",
|
|
617
|
+
"then": "Expected result",
|
|
618
|
+
"priority": "must-have|should-have|nice-to-have"
|
|
619
|
+
}
|
|
620
|
+
],
|
|
621
|
+
"acceptanceCriteria": ["criterion 1", "criterion 2"]
|
|
622
|
+
}
|
|
623
|
+
`.trim();
|
|
624
|
+
try {
|
|
625
|
+
const response = await this.llmProvider.generateText(prompt, {
|
|
626
|
+
maxTokens: 1000,
|
|
627
|
+
temperature: 0.3,
|
|
628
|
+
});
|
|
629
|
+
// Extract JSON from response (might have markdown code blocks)
|
|
630
|
+
let jsonText = response.text.trim();
|
|
631
|
+
// Try multiple patterns to be more robust
|
|
632
|
+
const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]+?)\s*```/);
|
|
633
|
+
if (jsonMatch) {
|
|
634
|
+
jsonText = jsonMatch[1].trim();
|
|
635
|
+
}
|
|
636
|
+
else if (jsonText.startsWith('```')) {
|
|
637
|
+
// Fallback: remove all triple backticks
|
|
638
|
+
jsonText = jsonText
|
|
639
|
+
.replace(/```[^\n]*\n?/g, '')
|
|
640
|
+
.replace(/```\s*$/g, '')
|
|
641
|
+
.trim();
|
|
642
|
+
}
|
|
643
|
+
const data = JSON.parse(jsonText);
|
|
644
|
+
const scenarios = (data.scenarios || []).map((s) => ({
|
|
645
|
+
name: s.name,
|
|
646
|
+
given: s.given,
|
|
647
|
+
when: s.when,
|
|
648
|
+
then: s.then,
|
|
649
|
+
priority: s.priority || 'should-have',
|
|
650
|
+
}));
|
|
651
|
+
return [
|
|
652
|
+
{
|
|
653
|
+
id: this.generateSpecId(data.feature),
|
|
654
|
+
name: data.feature,
|
|
655
|
+
description: `Generated from focus string: ${focusString}`,
|
|
656
|
+
priority: data.priority || 'medium',
|
|
657
|
+
targetUrls: data.targetUrls || [],
|
|
658
|
+
scenarios,
|
|
659
|
+
screenshots: [],
|
|
660
|
+
acceptanceCriteria: data.acceptanceCriteria || [],
|
|
661
|
+
sourcePath: 'focus-string',
|
|
662
|
+
},
|
|
663
|
+
];
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
throw new Error(`Failed to parse focus string with LLM: ${error instanceof Error ? error.message : String(error)}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Finalize partial spec
|
|
671
|
+
*/
|
|
672
|
+
finalizeSpec(partial, sourcePath) {
|
|
673
|
+
return {
|
|
674
|
+
id: this.generateSpecId(partial.name || 'unknown'),
|
|
675
|
+
name: partial.name || 'Unnamed Feature',
|
|
676
|
+
description: (partial.description || '').trim(),
|
|
677
|
+
priority: partial.priority || 'medium',
|
|
678
|
+
targetUrls: partial.targetUrls || [],
|
|
679
|
+
scenarios: partial.scenarios || [],
|
|
680
|
+
screenshots: partial.screenshots || [],
|
|
681
|
+
acceptanceCriteria: partial.acceptanceCriteria || [],
|
|
682
|
+
sourcePath,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Finalize partial scenario
|
|
687
|
+
*/
|
|
688
|
+
finalizeScenario(partial) {
|
|
689
|
+
return {
|
|
690
|
+
name: partial.name || 'Unnamed Scenario',
|
|
691
|
+
given: partial.given || '',
|
|
692
|
+
when: partial.when || '',
|
|
693
|
+
then: partial.then || '',
|
|
694
|
+
priority: partial.priority || 'should-have',
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Generate spec ID from name
|
|
699
|
+
*/
|
|
700
|
+
generateSpecId(name) {
|
|
701
|
+
return name
|
|
702
|
+
.toLowerCase()
|
|
703
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
704
|
+
.replace(/^-|-$/g, '');
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Resolve image path relative to spec file
|
|
708
|
+
*/
|
|
709
|
+
resolveImagePath(imagePath, specPath) {
|
|
710
|
+
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
|
|
711
|
+
return imagePath; // Absolute URL
|
|
712
|
+
}
|
|
713
|
+
if (imagePath.startsWith('/')) {
|
|
714
|
+
return imagePath; // Absolute path
|
|
715
|
+
}
|
|
716
|
+
// Relative path - resolve relative to spec file
|
|
717
|
+
const specDir = dirname(specPath);
|
|
718
|
+
return join(specDir, imagePath);
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Load screenshot data from files
|
|
722
|
+
*/
|
|
723
|
+
async loadScreenshots(specs) {
|
|
724
|
+
for (const spec of specs) {
|
|
725
|
+
for (const screenshot of spec.screenshots) {
|
|
726
|
+
try {
|
|
727
|
+
if (existsSync(screenshot.path)) {
|
|
728
|
+
const imageData = readFileSync(screenshot.path);
|
|
729
|
+
screenshot.data = imageData.toString('base64');
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
// eslint-disable-next-line no-console
|
|
733
|
+
console.warn(`Screenshot not found: ${screenshot.path}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
// eslint-disable-next-line no-console
|
|
738
|
+
console.warn(`Failed to load screenshot ${screenshot.path}:`, error);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Validate parsed specification
|
|
745
|
+
*/
|
|
746
|
+
validateSpec(spec) {
|
|
747
|
+
const errors = [];
|
|
748
|
+
if (!spec.name || spec.name.trim().length === 0) {
|
|
749
|
+
errors.push('Feature name is required');
|
|
750
|
+
}
|
|
751
|
+
if (spec.scenarios.length === 0) {
|
|
752
|
+
errors.push('At least one scenario is required');
|
|
753
|
+
}
|
|
754
|
+
for (let i = 0; i < spec.scenarios.length; i++) {
|
|
755
|
+
const scenario = spec.scenarios[i];
|
|
756
|
+
if (!scenario.given || !scenario.when || !scenario.then) {
|
|
757
|
+
errors.push(`Scenario ${i + 1} is incomplete (missing given/when/then)`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
valid: errors.length === 0,
|
|
762
|
+
errors,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Get spec coverage summary
|
|
767
|
+
*/
|
|
768
|
+
getSpecSummary(spec) {
|
|
769
|
+
return {
|
|
770
|
+
id: spec.id,
|
|
771
|
+
name: spec.name,
|
|
772
|
+
priority: spec.priority,
|
|
773
|
+
targetUrlCount: spec.targetUrls.length,
|
|
774
|
+
scenarioCount: spec.scenarios.length,
|
|
775
|
+
mustHaveScenarios: spec.scenarios.filter((s) => s.priority === 'must-have').length,
|
|
776
|
+
shouldHaveScenarios: spec.scenarios.filter((s) => s.priority === 'should-have').length,
|
|
777
|
+
niceToHaveScenarios: spec.scenarios.filter((s) => s.priority === 'nice-to-have').length,
|
|
778
|
+
acceptanceCriteriaCount: spec.acceptanceCriteria.length,
|
|
779
|
+
hasScreenshots: spec.screenshots.length > 0,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
}
|