validpilot-oss 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +111 -0
- package/README.md +196 -0
- package/bin/validpilot.js +173 -0
- package/brain/error_aggregator.js +203 -0
- package/core/artifacts.js +44 -0
- package/core/config.js +37 -0
- package/core/redaction.js +39 -0
- package/core/report.js +42 -0
- package/core/result.js +29 -0
- package/core/security.js +57 -0
- package/engines/chrome_mcp_adapter.js +319 -0
- package/engines/playwright_adapter.js +421 -0
- package/examples/demo/README.md +58 -0
- package/examples/demo/diagnostic-error-flow.json +22 -0
- package/examples/demo/diagnostic-error.html +29 -0
- package/examples/demo/flow.json +27 -0
- package/examples/demo/index.html +29 -0
- package/hands/browser_operator.js +67 -0
- package/hands/evidence_collector.js +97 -0
- package/package.json +55 -0
- package/rules/suggested-rules.json +237 -0
- package/server.js +5376 -0
- package/standalone-start.js +43 -0
- package/start-http.js +45 -0
- package/tools/ai_debug_investigate.json +30 -0
- package/tools/benchmark_run.json +37 -0
- package/tools/browser_a11y_check.json +21 -0
- package/tools/browser_artifacts.json +8 -0
- package/tools/browser_artifacts_clear.json +11 -0
- package/tools/browser_assert.json +16 -0
- package/tools/browser_batch.json +61 -0
- package/tools/browser_click.json +11 -0
- package/tools/browser_console.json +26 -0
- package/tools/browser_cookies.json +38 -0
- package/tools/browser_debug_report.json +11 -0
- package/tools/browser_diagnose.json +23 -0
- package/tools/browser_dom.json +11 -0
- package/tools/browser_element_status.json +26 -0
- package/tools/browser_errors.json +17 -0
- package/tools/browser_errors_aggregate.json +12 -0
- package/tools/browser_errors_clear.json +8 -0
- package/tools/browser_eval.json +11 -0
- package/tools/browser_events.json +15 -0
- package/tools/browser_events_clear.json +8 -0
- package/tools/browser_find_element.json +30 -0
- package/tools/browser_find_page.json +22 -0
- package/tools/browser_flow.json +38 -0
- package/tools/browser_har_export.json +17 -0
- package/tools/browser_highlight.json +18 -0
- package/tools/browser_hover.json +14 -0
- package/tools/browser_instrument.json +10 -0
- package/tools/browser_links.json +21 -0
- package/tools/browser_locator_suggest.json +16 -0
- package/tools/browser_locator_validate.json +12 -0
- package/tools/browser_network.json +16 -0
- package/tools/browser_network_detail.json +17 -0
- package/tools/browser_open.json +12 -0
- package/tools/browser_performance_check.json +25 -0
- package/tools/browser_press_key.json +18 -0
- package/tools/browser_quick_fix.json +29 -0
- package/tools/browser_screenshot.json +15 -0
- package/tools/browser_scroll.json +31 -0
- package/tools/browser_select.json +26 -0
- package/tools/browser_session_close.json +12 -0
- package/tools/browser_session_create.json +17 -0
- package/tools/browser_session_switch.json +12 -0
- package/tools/browser_sessions.json +8 -0
- package/tools/browser_snapshot.json +8 -0
- package/tools/browser_step.json +18 -0
- package/tools/browser_storage.json +10 -0
- package/tools/browser_trace_start.json +14 -0
- package/tools/browser_trace_stop.json +10 -0
- package/tools/browser_traverse_menu.json +25 -0
- package/tools/browser_type.json +12 -0
- package/tools/browser_verify_fix.json +39 -0
- package/tools/browser_visual_baseline.json +19 -0
- package/tools/browser_visual_compare.json +20 -0
- package/tools/browser_visual_report.json +8 -0
- package/tools/browser_wait.json +18 -0
- package/tools/debug_investigate.json +17 -0
- package/tools/error_fix_suggestion.json +13 -0
- package/tools/error_summary_md.json +11 -0
- package/tools/fix_verify.json +13 -0
- package/tools/mcp_health_check.json +8 -0
- package/tools/mcp_self_test.json +12 -0
- package/tools/screenshot_diff.json +16 -0
- package/tools/validation_check.json +20 -0
- package/tools/validation_decision.json +24 -0
- package/tools/validation_element.json +13 -0
- package/tools/validation_flow.json +12 -0
- package/tools/validation_matrix.json +27 -0
- package/tools/validation_quick_run.json +13 -0
- package/tools/validation_report.json +10 -0
- package/tools/validation_report_export.json +8 -0
- package/tools/validation_run.json +35 -0
- package/tools/validation_start.json +12 -0
- package/tools/validation_suite_run.json +17 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const ARTIFACT_DIR = path.join(__dirname, '..', 'artifacts', 'phase1');
|
|
7
|
+
|
|
8
|
+
function ensureDir(dir = ARTIFACT_DIR) {
|
|
9
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function safeName(name) {
|
|
14
|
+
return String(name || `artifact-${Date.now()}`).replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toFileUrl(input) {
|
|
18
|
+
const value = String(input || '');
|
|
19
|
+
if (/^https?:\/\//i.test(value) || /^file:\/\//i.test(value)) return value;
|
|
20
|
+
return `file://${path.resolve(value).replace(/\\/g, '/')}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function redactString(value) {
|
|
24
|
+
return String(value ?? '')
|
|
25
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+\/-]+=*/gi, 'Bearer ******')
|
|
26
|
+
.replace(/(api[_-]?key\s*[:=]\s*)[A-Za-z0-9._~+\/-]{8,}/gi, '$1******')
|
|
27
|
+
.replace(/(token\s*[:=]\s*)[A-Za-z0-9._~+\/-]{8,}/gi, '$1******')
|
|
28
|
+
.slice(0, 2000);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function truncate(value, max = 500) {
|
|
32
|
+
const text = redactString(value);
|
|
33
|
+
return text.length > max ? `${text.slice(0, max)}...` : text;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function summarizeEntries(entries = [], limit = 10) {
|
|
37
|
+
return entries.slice(-limit).map(item => {
|
|
38
|
+
const summary = {
|
|
39
|
+
source: item.source,
|
|
40
|
+
type: item.type,
|
|
41
|
+
text: truncate(item.text || item.message || item.errorText || item.url || '', 240),
|
|
42
|
+
url: item.url,
|
|
43
|
+
status: item.status,
|
|
44
|
+
method: item.method,
|
|
45
|
+
failed: item.failed === true,
|
|
46
|
+
timestamp: item.timestamp
|
|
47
|
+
};
|
|
48
|
+
return Object.fromEntries(Object.entries(summary).filter(([, value]) => value !== undefined && value !== ''));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class PlaywrightAdapter {
|
|
53
|
+
constructor(options = {}) {
|
|
54
|
+
this.options = { headless: true, viewport: { width: 1280, height: 800 }, ...options };
|
|
55
|
+
this.browser = null;
|
|
56
|
+
this.page = null;
|
|
57
|
+
this.consoleLogs = [];
|
|
58
|
+
this.networkLogs = [];
|
|
59
|
+
this.pageErrors = [];
|
|
60
|
+
this.artifactDir = options.artifactDir || ARTIFACT_DIR;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async ensurePage(options = {}) {
|
|
64
|
+
if (!this.browser) {
|
|
65
|
+
let chromium;
|
|
66
|
+
try {
|
|
67
|
+
chromium = require('playwright').chromium;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw new Error(`Playwright dependency is unavailable: ${error.message}`);
|
|
70
|
+
}
|
|
71
|
+
this.browser = await chromium.launch({ headless: options.headless ?? this.options.headless });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!this.page || this.page.isClosed()) {
|
|
75
|
+
this.page = await this.browser.newPage({ viewport: options.viewport || this.options.viewport });
|
|
76
|
+
this.attachListeners(this.page);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return this.page;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
attachListeners(page) {
|
|
83
|
+
page.on('console', msg => {
|
|
84
|
+
this.consoleLogs.push({
|
|
85
|
+
source: 'console',
|
|
86
|
+
type: msg.type(),
|
|
87
|
+
text: redactString(msg.text()),
|
|
88
|
+
location: msg.location(),
|
|
89
|
+
timestamp: new Date().toISOString()
|
|
90
|
+
});
|
|
91
|
+
this.trimLogs();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
page.on('pageerror', error => {
|
|
95
|
+
this.pageErrors.push({
|
|
96
|
+
source: 'pageerror',
|
|
97
|
+
type: 'error',
|
|
98
|
+
text: truncate(error.message, 800),
|
|
99
|
+
stack: truncate(error.stack, 1200),
|
|
100
|
+
timestamp: new Date().toISOString()
|
|
101
|
+
});
|
|
102
|
+
this.trimLogs();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
page.on('response', response => {
|
|
106
|
+
const request = response.request();
|
|
107
|
+
const status = response.status();
|
|
108
|
+
if (status >= 400) {
|
|
109
|
+
this.networkLogs.push({
|
|
110
|
+
source: 'network',
|
|
111
|
+
url: response.url(),
|
|
112
|
+
status,
|
|
113
|
+
method: request.method(),
|
|
114
|
+
timestamp: new Date().toISOString()
|
|
115
|
+
});
|
|
116
|
+
this.trimLogs();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
page.on('requestfailed', request => {
|
|
121
|
+
this.networkLogs.push({
|
|
122
|
+
source: 'network',
|
|
123
|
+
url: request.url(),
|
|
124
|
+
method: request.method(),
|
|
125
|
+
failed: true,
|
|
126
|
+
errorText: request.failure()?.errorText,
|
|
127
|
+
timestamp: new Date().toISOString()
|
|
128
|
+
});
|
|
129
|
+
this.trimLogs();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
trimLogs() {
|
|
134
|
+
this.consoleLogs = this.consoleLogs.slice(-300);
|
|
135
|
+
this.networkLogs = this.networkLogs.slice(-300);
|
|
136
|
+
this.pageErrors = this.pageErrors.slice(-100);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async open(args = {}) {
|
|
140
|
+
if (!args.url) throw new Error('browser open requires url');
|
|
141
|
+
const page = await this.ensurePage(args);
|
|
142
|
+
await page.goto(toFileUrl(args.url), { waitUntil: args.waitUntil || 'domcontentloaded', timeout: args.timeout || 30000 });
|
|
143
|
+
return { ok: true, action: 'open', url: page.url(), title: await page.title().catch(() => '') };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async click(args = {}) {
|
|
147
|
+
const page = await this.ensurePage(args);
|
|
148
|
+
await page.click(args.selector, { timeout: args.timeout || 10000 });
|
|
149
|
+
return { ok: true, action: 'click', selector: args.selector };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async type(args = {}) {
|
|
153
|
+
const page = await this.ensurePage(args);
|
|
154
|
+
await page.fill(args.selector, String(args.text ?? ''), { timeout: args.timeout || 10000 });
|
|
155
|
+
return { ok: true, action: 'type', selector: args.selector, textLength: String(args.text ?? '').length };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async wait(args = {}) {
|
|
159
|
+
const page = await this.ensurePage(args);
|
|
160
|
+
if (args.selector) {
|
|
161
|
+
await page.waitForSelector(args.selector, { timeout: args.timeout || 10000, state: args.state || 'visible' });
|
|
162
|
+
return { ok: true, action: 'wait', selector: args.selector, state: args.state || 'visible' };
|
|
163
|
+
}
|
|
164
|
+
if (args.ms || args.timeout) {
|
|
165
|
+
await page.waitForTimeout(Math.min(Number(args.ms || args.timeout), 10000));
|
|
166
|
+
return { ok: true, action: 'wait', ms: Math.min(Number(args.ms || args.timeout), 10000) };
|
|
167
|
+
}
|
|
168
|
+
await page.waitForLoadState(args.state || 'domcontentloaded', { timeout: args.timeout || 10000 }).catch(() => {});
|
|
169
|
+
return { ok: true, action: 'wait', state: args.state || 'domcontentloaded' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async eval(args = {}) {
|
|
173
|
+
const page = await this.ensurePage(args);
|
|
174
|
+
const expression = args.expression || args.script;
|
|
175
|
+
if (!expression) throw new Error('eval requires expression');
|
|
176
|
+
const result = await page.evaluate(source => {
|
|
177
|
+
const value = (0, eval)(source);
|
|
178
|
+
return typeof value === 'undefined' ? null : value;
|
|
179
|
+
}, expression);
|
|
180
|
+
const serialized = JSON.stringify(result);
|
|
181
|
+
if (serialized && serialized.length > 2000) {
|
|
182
|
+
const artifactPath = this.writeArtifact('eval-result', result);
|
|
183
|
+
return { ok: true, action: 'eval', resultSummary: truncate(serialized, 500), artifactPath };
|
|
184
|
+
}
|
|
185
|
+
return { ok: true, action: 'eval', result };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async screenshot(args = {}) {
|
|
189
|
+
const page = await this.ensurePage(args);
|
|
190
|
+
ensureDir(this.artifactDir);
|
|
191
|
+
const filePath = args.path || path.join(this.artifactDir, `${safeName(args.name || 'screenshot')}-${Date.now()}.png`);
|
|
192
|
+
const target = args.selector ? page.locator(args.selector).first() : page;
|
|
193
|
+
await target.screenshot({ path: filePath, fullPage: !args.selector && args.fullPage !== false });
|
|
194
|
+
return { ok: true, action: 'screenshot', artifactPath: filePath, summary: 'screenshot saved; no long image description returned' };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async batch(args = {}) {
|
|
198
|
+
const steps = Array.isArray(args.steps) ? args.steps : [];
|
|
199
|
+
const results = [];
|
|
200
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
201
|
+
const step = steps[index] || {};
|
|
202
|
+
const action = step.action || step.type;
|
|
203
|
+
const stepArgs = step.args && typeof step.args === 'object' ? { ...step.args } : {};
|
|
204
|
+
const merged = { ...args, ...stepArgs, ...step, action };
|
|
205
|
+
try {
|
|
206
|
+
const result = await this.runAction(action, merged);
|
|
207
|
+
results.push({ index, action, ok: true, summary: summarizeResult(result), artifactPath: result.artifactPath });
|
|
208
|
+
} catch (error) {
|
|
209
|
+
results.push({ index, action, ok: false, error: truncate(error.message, 300) });
|
|
210
|
+
if (args.stopOnError !== false) break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { ok: results.every(item => item.ok), action: 'batch', stepCount: steps.length, results, evidence: await this.collectEvidenceSummary() };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async hover(args = {}) {
|
|
217
|
+
const page = await this.ensurePage(args);
|
|
218
|
+
await page.hover(args.selector, { timeout: args.timeout || 10000 });
|
|
219
|
+
return { ok: true, action: 'hover', selector: args.selector };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async scroll(args = {}) {
|
|
223
|
+
const page = await this.ensurePage(args);
|
|
224
|
+
if (args.selector) {
|
|
225
|
+
await page.$eval(args.selector, el => el.scrollIntoView({ block: args.block || 'center', inline: args.inline || 'center' }));
|
|
226
|
+
} else {
|
|
227
|
+
await page.evaluate(({ x, y }) => window.scrollTo(x || 0, y || 0), { x: args.x || 0, y: args.y || args.distance || 300 });
|
|
228
|
+
}
|
|
229
|
+
return { ok: true, action: 'scroll', selector: args.selector || null };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async pressKey(args = {}) {
|
|
233
|
+
const page = await this.ensurePage(args);
|
|
234
|
+
if (args.selector) await page.focus(args.selector);
|
|
235
|
+
await page.keyboard.press(args.key);
|
|
236
|
+
return { ok: true, action: 'press_key', key: args.key };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async errors(args = {}) {
|
|
240
|
+
const includeWarnings = args.includeWarnings === true;
|
|
241
|
+
let filtered = [];
|
|
242
|
+
|
|
243
|
+
if (includeWarnings) {
|
|
244
|
+
filtered.push(...this.consoleLogs.filter(item => item.type === 'error' || item.type === 'warning'));
|
|
245
|
+
} else {
|
|
246
|
+
filtered.push(...this.consoleLogs.filter(item => item.type === 'error'));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
filtered.push(...this.pageErrors);
|
|
250
|
+
filtered.push(...this.networkLogs.filter(item => item.status >= 400 || item.failed));
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
action: 'errors',
|
|
254
|
+
total: filtered.length,
|
|
255
|
+
console: this.consoleLogs.length,
|
|
256
|
+
network: this.networkLogs.length,
|
|
257
|
+
pageError: this.pageErrors.length,
|
|
258
|
+
errors: summarizeEntries(filtered, args.limit || 20)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async errorsClear(args = {}) {
|
|
263
|
+
this.consoleLogs = [];
|
|
264
|
+
this.networkLogs = [];
|
|
265
|
+
this.pageErrors = [];
|
|
266
|
+
this.errorCheckpoint = new Date().toISOString();
|
|
267
|
+
return { action: 'errors_clear', checkpoint: this.errorCheckpoint, cleared: true };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async artifacts(args = {}) {
|
|
271
|
+
const artifactDir = this.artifactDir;
|
|
272
|
+
const files = fs.existsSync(artifactDir) ? fs.readdirSync(artifactDir).slice(-20) : [];
|
|
273
|
+
return {
|
|
274
|
+
action: 'artifacts',
|
|
275
|
+
dir: artifactDir,
|
|
276
|
+
files,
|
|
277
|
+
count: files.length
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async runAction(action, args = {}) {
|
|
282
|
+
switch (action) {
|
|
283
|
+
case 'open': return this.open(args);
|
|
284
|
+
case 'navigate': return this.open(args);
|
|
285
|
+
case 'click': return this.click(args);
|
|
286
|
+
case 'type': return this.type(args);
|
|
287
|
+
case 'hover': return this.hover(args);
|
|
288
|
+
case 'scroll': return this.scroll(args);
|
|
289
|
+
case 'press_key': return this.pressKey(args);
|
|
290
|
+
case 'wait': return this.wait(args);
|
|
291
|
+
case 'eval': return this.eval(args);
|
|
292
|
+
case 'screenshot': return this.screenshot(args);
|
|
293
|
+
case 'batch': return this.batch(args);
|
|
294
|
+
case 'errors': return this.errors(args);
|
|
295
|
+
case 'errors_clear': return this.errorsClear(args);
|
|
296
|
+
case 'artifacts': return this.artifacts(args);
|
|
297
|
+
case 'summary': return this.collectEvidenceSummary(args);
|
|
298
|
+
case 'check': return this.checkAction(args);
|
|
299
|
+
case 'collect': return this.collectAction(args);
|
|
300
|
+
case 'report': return this.reportAction(args);
|
|
301
|
+
default: throw new Error(`unsupported browser action: ${action}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async checkAction(args = {}) {
|
|
306
|
+
const checks = Array.isArray(args.checks) ? args.checks : [];
|
|
307
|
+
const evidence = await this.collectEvidenceSummary();
|
|
308
|
+
const violations = [];
|
|
309
|
+
if (args.selector) {
|
|
310
|
+
const page = await this.ensurePage(args);
|
|
311
|
+
const el = await page.$(args.selector);
|
|
312
|
+
if (!el) {
|
|
313
|
+
violations.push({ check: 'selector', selector: args.selector, detail: 'element not found' });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const check of checks) {
|
|
317
|
+
if (check === 'no_top_errors' || check === 'no_errors') {
|
|
318
|
+
if (evidence.topErrors && evidence.topErrors.length > 0) {
|
|
319
|
+
violations.push({ check, detail: `${evidence.topErrors.length} top errors found` });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return { action: 'check', checks, pass: violations.length === 0, violations };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async collectAction(args = {}) {
|
|
327
|
+
const evidenceTypes = Array.isArray(args.evidence) ? args.evidence : ['console'];
|
|
328
|
+
const collected = {};
|
|
329
|
+
for (const type of evidenceTypes) {
|
|
330
|
+
if (type === 'console') collected.console = this.consoleLogs.slice(-50);
|
|
331
|
+
else if (type === 'pageerror') collected.pageerror = this.pageErrors.slice(-50);
|
|
332
|
+
else if (type === 'network') collected.network = this.networkLogs.slice(-50);
|
|
333
|
+
}
|
|
334
|
+
return { action: 'collect', evidence: evidenceTypes, collected, summary: summarizeResult(collected) };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async reportAction(args = {}) {
|
|
338
|
+
const evidence = await this.collectEvidenceSummary();
|
|
339
|
+
const result = {
|
|
340
|
+
action: 'report',
|
|
341
|
+
pass: (evidence.topErrors || []).length === 0,
|
|
342
|
+
mode: args.format || 'short',
|
|
343
|
+
topErrors: evidence.topErrors || [],
|
|
344
|
+
artifacts: evidence.artifactPath ? [evidence.artifactPath] : []
|
|
345
|
+
};
|
|
346
|
+
result.summary = result.pass ? 'pass' : `fail: ${result.topErrors.length} top errors`;
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async domSummary(args = {}) {
|
|
351
|
+
const page = await this.ensurePage(args);
|
|
352
|
+
return page.evaluate(() => {
|
|
353
|
+
const text = (document.body && document.body.innerText || '').replace(/\s+/g, ' ').trim();
|
|
354
|
+
const stableSelectorFor = el => {
|
|
355
|
+
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
356
|
+
const testId = el.getAttribute('data-testid') || el.getAttribute('data-test') || el.getAttribute('data-qa');
|
|
357
|
+
if (testId) return `[data-testid="${testId}"]`;
|
|
358
|
+
const name = el.getAttribute('name');
|
|
359
|
+
if (name) return `${el.tagName.toLowerCase()}[name="${name}"]`;
|
|
360
|
+
const role = el.getAttribute('role');
|
|
361
|
+
const label = (el.getAttribute('aria-label') || el.innerText || el.textContent || '').trim();
|
|
362
|
+
if (role && label) return `[role="${role}"][aria-label="${label.slice(0, 40)}"]`;
|
|
363
|
+
return el.tagName.toLowerCase();
|
|
364
|
+
};
|
|
365
|
+
const controls = Array.from(document.querySelectorAll('button,a,input,textarea,select,[role="button"],[role="link"]')).slice(0, 30).map(el => ({
|
|
366
|
+
selector: stableSelectorFor(el),
|
|
367
|
+
tag: el.tagName.toLowerCase(),
|
|
368
|
+
role: el.getAttribute('role') || '',
|
|
369
|
+
text: (el.innerText || el.getAttribute('aria-label') || el.getAttribute('placeholder') || '').trim().slice(0, 80),
|
|
370
|
+
visible: !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length),
|
|
371
|
+
disabled: !!el.disabled || el.getAttribute('aria-disabled') === 'true'
|
|
372
|
+
}));
|
|
373
|
+
const alerts = Array.from(document.querySelectorAll('[role="alert"],.error,.alert,.toast')).slice(0, 10).map(el => (el.innerText || el.textContent || '').trim().slice(0, 160));
|
|
374
|
+
return { url: location.href, title: document.title, readyState: document.readyState, textSummary: text.slice(0, 1000), controls, alerts };
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async collectEvidenceSummary(args = {}) {
|
|
379
|
+
const dom = this.page && !this.page.isClosed() ? await this.domSummary(args).catch(error => ({ error: error.message })) : null;
|
|
380
|
+
// 使用类内部的存储(开源版)
|
|
381
|
+
return {
|
|
382
|
+
generatedAt: new Date().toISOString(),
|
|
383
|
+
console: { count: this.consoleLogs.length, recent: summarizeEntries(this.consoleLogs, args.limit || 10) },
|
|
384
|
+
network: { count: this.networkLogs.length, recent: summarizeEntries(this.networkLogs, args.limit || 10) },
|
|
385
|
+
pageerror: { count: this.pageErrors.length, recent: summarizeEntries(this.pageErrors, args.limit || 10) },
|
|
386
|
+
dom
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
writeArtifact(name, data) {
|
|
391
|
+
ensureDir(this.artifactDir);
|
|
392
|
+
const filePath = path.join(this.artifactDir, `${safeName(name)}-${Date.now()}.json`);
|
|
393
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
394
|
+
return filePath;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async close() {
|
|
398
|
+
if (this.browser) await this.browser.close().catch(() => {});
|
|
399
|
+
this.browser = null;
|
|
400
|
+
this.page = null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function summarizeResult(result = {}) {
|
|
405
|
+
if (result.summary) return result.summary;
|
|
406
|
+
if (result.action === 'open') return `opened ${result.url}`;
|
|
407
|
+
if (result.action) return `${result.action} ok`;
|
|
408
|
+
return 'ok';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const defaultAdapter = new PlaywrightAdapter();
|
|
412
|
+
|
|
413
|
+
module.exports = {
|
|
414
|
+
PlaywrightAdapter,
|
|
415
|
+
defaultAdapter,
|
|
416
|
+
toFileUrl,
|
|
417
|
+
ensureDir,
|
|
418
|
+
redactString,
|
|
419
|
+
truncate,
|
|
420
|
+
summarizeEntries
|
|
421
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# ValidPilot 低 Token Demo
|
|
2
|
+
|
|
3
|
+
> **声明:本 Demo 仅用于快速验证演示,不是 E2E 测试体系。**
|
|
4
|
+
|
|
5
|
+
这是一个纯静态页面的低 Token 验证 Demo,不依赖 E2E 框架、不绑定任何外部业务项目,仅用于 3 分钟快速体验 ValidPilot 的核心摘要能力。
|
|
6
|
+
|
|
7
|
+
## 样例用途
|
|
8
|
+
|
|
9
|
+
| 样例 | 文件 | 用途 |
|
|
10
|
+
|------|------|------|
|
|
11
|
+
| Clean demo | `index.html` + `flow.json` | 默认 business_pass 快速验证样例,初始加载不主动产生 console error、pageerror 或失败网络请求 |
|
|
12
|
+
| Diagnostic error demo | `diagnostic-error.html` + `diagnostic-error-flow.json` | 故意产生 console error/warn,仅用于错误聚合、根因分析和修复建议验证 |
|
|
13
|
+
|
|
14
|
+
## 3 分钟体验
|
|
15
|
+
|
|
16
|
+
### 前置条件
|
|
17
|
+
- Node.js >= 18
|
|
18
|
+
- 已安装依赖(`npm install`)
|
|
19
|
+
|
|
20
|
+
### 步骤 1 — 健康检查
|
|
21
|
+
```bash
|
|
22
|
+
node bin/validpilot.js health
|
|
23
|
+
```
|
|
24
|
+
预期:输出 MCP 可用性和引擎状态摘要。
|
|
25
|
+
|
|
26
|
+
### 步骤 2 — 打开 Clean Demo 页面并获取摘要
|
|
27
|
+
```bash
|
|
28
|
+
node bin/validpilot.js validate --url examples/demo/index.html
|
|
29
|
+
```
|
|
30
|
+
预期:输出 pass=true、Top errors 为空、DOM 摘要和短 artifact 路径。
|
|
31
|
+
|
|
32
|
+
### 步骤 3 — 执行 Clean 轻量验证流
|
|
33
|
+
```bash
|
|
34
|
+
node bin/validpilot.js run --flow examples/demo/flow.json
|
|
35
|
+
```
|
|
36
|
+
预期:依次执行 open → check → click → check → report,点击主按钮后状态变为成功文案,并返回 business_pass 用短报告。
|
|
37
|
+
|
|
38
|
+
### 显式运行 Diagnostic Error 样例
|
|
39
|
+
```bash
|
|
40
|
+
node bin/validpilot.js validate --url examples/demo/diagnostic-error.html
|
|
41
|
+
node bin/validpilot.js run --flow examples/demo/diagnostic-error-flow.json
|
|
42
|
+
```
|
|
43
|
+
预期:用于观察 intentional error/warn 的聚合摘要和修复建议输入,不应作为默认 business_pass 样例。
|
|
44
|
+
|
|
45
|
+
## 文件说明
|
|
46
|
+
| 文件 | 用途 |
|
|
47
|
+
|------|------|
|
|
48
|
+
| `index.html` | clean 静态页面,包含标题、说明、主按钮和状态元素 |
|
|
49
|
+
| `flow.json` | clean 轻量验证流定义(JSON 数组格式) |
|
|
50
|
+
| `diagnostic-error.html` | 含故意 console error ×2 和 warn ×1 的诊断页面 |
|
|
51
|
+
| `diagnostic-error-flow.json` | diagnostic error 轻量验证流定义 |
|
|
52
|
+
| `README.md` | 本文档 |
|
|
53
|
+
|
|
54
|
+
## 注意事项
|
|
55
|
+
- 此 Demo 不引用任何外部业务项目页面、接口或功能
|
|
56
|
+
- 不创建 E2E 目录、E2E spec 或重型浏览器回归脚本
|
|
57
|
+
- 输出仅包含短摘要,不输出完整 DOM、完整日志或长截图描述
|
|
58
|
+
- 默认 CLI 示例优先使用 clean demo;error demo 必须显式指定文件运行
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"action": "open",
|
|
4
|
+
"args": { "url": "examples/demo/diagnostic-error.html" },
|
|
5
|
+
"expect": "diagnostic error demo 页面加载成功"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"action": "summary",
|
|
9
|
+
"args": { "mode": "low-token" },
|
|
10
|
+
"expect": "返回控制台摘要,包含至少 2 条 error 和 1 条 warn"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"action": "check",
|
|
14
|
+
"args": { "selector": "[data-testid=\"demo-action\"]" },
|
|
15
|
+
"expect": "诊断样例主按钮存在且可见"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"action": "report",
|
|
19
|
+
"args": { "format": "short" },
|
|
20
|
+
"expect": "输出错误聚合用轻量报告,包含 Top errors 摘要及 artifact 路径"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>ValidPilot Diagnostic Error Demo</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: system-ui, sans-serif; max-width: 760px; margin: 40px auto; padding: 0 20px; line-height: 1.6; }
|
|
9
|
+
main { border: 1px solid #d0d7de; border-radius: 12px; padding: 24px; }
|
|
10
|
+
button { padding: 8px 14px; border-radius: 8px; border: 1px solid #8250df; background: #8250df; color: white; cursor: pointer; }
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<main>
|
|
15
|
+
<h1>ValidPilot Diagnostic Error Demo</h1>
|
|
16
|
+
<p>此页面包含故意的控制台错误与警告,仅用于验证错误聚合、根因分析和修复建议,不作为 business_pass 默认样例。</p>
|
|
17
|
+
<button id="demo-action" data-testid="demo-action" type="button">触发诊断状态</button>
|
|
18
|
+
<p id="status" role="status">等待操作</p>
|
|
19
|
+
</main>
|
|
20
|
+
<script>
|
|
21
|
+
console.error('Demo: intentional error #1 — 模拟未捕获的类型错误');
|
|
22
|
+
console.warn('Demo: intentional warning — 模拟废弃 API 调用');
|
|
23
|
+
console.error('Demo: intentional error #2 — 模拟网络请求失败');
|
|
24
|
+
document.getElementById('demo-action').addEventListener('click', function () {
|
|
25
|
+
document.getElementById('status').textContent = '已触发诊断状态';
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"action": "open",
|
|
4
|
+
"args": { "url": "examples/demo/index.html" },
|
|
5
|
+
"expect": "clean demo 页面加载成功,且不产生 console error/pageerror/失败网络请求"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"action": "check",
|
|
9
|
+
"args": { "selector": "[data-testid=\"demo-action\"]", "checks": ["no_top_errors"] },
|
|
10
|
+
"expect": "主按钮存在且当前无 top browser errors"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"action": "click",
|
|
14
|
+
"args": { "selector": "[data-testid=\"demo-action\"]" },
|
|
15
|
+
"expect": "点击主按钮不产生 runtime error"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"action": "check",
|
|
19
|
+
"args": { "selector": "#status[data-state=\"success\"]", "checks": ["no_top_errors"] },
|
|
20
|
+
"expect": "状态元素变为成功状态且仍无 top browser errors"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"action": "report",
|
|
24
|
+
"args": { "format": "short" },
|
|
25
|
+
"expect": "输出 business_pass 用短报告,pass=true 且 Top errors 为空"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>ValidPilot Clean Demo</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: system-ui, sans-serif; max-width: 760px; margin: 40px auto; padding: 0 20px; line-height: 1.6; }
|
|
9
|
+
main { border: 1px solid #d0d7de; border-radius: 12px; padding: 24px; }
|
|
10
|
+
button { padding: 8px 14px; border-radius: 8px; border: 1px solid #0969da; background: #0969da; color: white; cursor: pointer; }
|
|
11
|
+
[role="status"] { margin-top: 16px; }
|
|
12
|
+
</style>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<main>
|
|
16
|
+
<h1>ValidPilot Clean Demo</h1>
|
|
17
|
+
<p>这是用于 business_pass 快速验证的干净静态页面,初始加载不主动产生控制台错误、页面错误或失败网络请求。</p>
|
|
18
|
+
<button id="demo-action" data-testid="demo-action" type="button">完成主流程</button>
|
|
19
|
+
<p id="status" data-testid="status" data-state="idle" role="status">等待操作</p>
|
|
20
|
+
</main>
|
|
21
|
+
<script>
|
|
22
|
+
document.getElementById('demo-action').addEventListener('click', function () {
|
|
23
|
+
var status = document.getElementById('status');
|
|
24
|
+
status.textContent = '验证成功:主流程已完成';
|
|
25
|
+
status.setAttribute('data-state', 'success');
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { defaultAdapter } = require('./../engines/playwright_adapter');
|
|
4
|
+
|
|
5
|
+
async function open(args = {}) {
|
|
6
|
+
return defaultAdapter.open(args);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function click(args = {}) {
|
|
10
|
+
return defaultAdapter.click(args);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function type(args = {}) {
|
|
14
|
+
return defaultAdapter.type(args);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function wait(args = {}) {
|
|
18
|
+
return defaultAdapter.wait(args);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function evalInPage(args = {}) {
|
|
22
|
+
return defaultAdapter.eval(args);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function screenshot(args = {}) {
|
|
26
|
+
return defaultAdapter.screenshot(args);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function batch(args = {}) {
|
|
30
|
+
return defaultAdapter.batch(args);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function navigate(args = {}) {
|
|
34
|
+
return defaultAdapter.open(args);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function summary(args = {}) {
|
|
38
|
+
return defaultAdapter.collectEvidenceSummary(args);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function checkAction(args = {}) {
|
|
42
|
+
return defaultAdapter.checkAction(args);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function collectAction(args = {}) {
|
|
46
|
+
return defaultAdapter.collectAction(args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function reportAction(args = {}) {
|
|
50
|
+
return defaultAdapter.reportAction(args);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
open,
|
|
55
|
+
navigate,
|
|
56
|
+
click,
|
|
57
|
+
type,
|
|
58
|
+
wait,
|
|
59
|
+
eval: evalInPage,
|
|
60
|
+
screenshot,
|
|
61
|
+
batch,
|
|
62
|
+
summary,
|
|
63
|
+
check: checkAction,
|
|
64
|
+
collect: collectAction,
|
|
65
|
+
report: reportAction,
|
|
66
|
+
adapter: defaultAdapter
|
|
67
|
+
};
|