@vatzzza/botintern 1.0.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/.github/workflows/npm-publish.yml +33 -0
- package/README.md +15 -0
- package/bun.lock +80 -0
- package/index.js +425 -0
- package/lib/ai.js +531 -0
- package/lib/aiFileParser.js +54 -0
- package/lib/fileTreeParser.js +105 -0
- package/lib/loop.js +174 -0
- package/lib/registerAndFetchKey.js +75 -0
- package/lib/runPlaywright.js +39 -0
- package/lib/runTests.js +418 -0
- package/lib/scan.js +58 -0
- package/package.json +29 -0
- package/tsconfig.json +29 -0
- package/utils/errorExtractor.js +33 -0
- package/utils/setupPlaywright.js +29 -0
- package/utils/yaml-example.txt +21 -0
- package/utils/yamlGuardrails.js +130 -0
package/lib/runTests.js
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import net from 'net';
|
|
7
|
+
|
|
8
|
+
// --- 1. HELPER: Normalize Actions (FIXED PRIORITY) ---
|
|
9
|
+
function normalizeAction(step) {
|
|
10
|
+
// 1. PRIORITY CHECK: Smart Type Shorthand
|
|
11
|
+
// We MUST check this first because the key is named 'type', which confuses the logic below.
|
|
12
|
+
if (step.type && step.into) {
|
|
13
|
+
return { type: 'type_smart', value: step.type, label: step.into };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 2. Legacy/Developer Syntax (Explicit 'type' key)
|
|
17
|
+
if (step.type) {
|
|
18
|
+
// If it uses "selector", it's the old developer syntax.
|
|
19
|
+
// We rename it to 'type_selector' to distinguish it from the smart syntax.
|
|
20
|
+
if (step.type === 'type' && step.selector) {
|
|
21
|
+
return { type: 'type_selector', selector: step.selector, value: step.value };
|
|
22
|
+
}
|
|
23
|
+
// Return other explicit types (assert_text, assert_visible) as is
|
|
24
|
+
return step;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 3. Product Manager Shorthands
|
|
28
|
+
if (step.see) return { type: 'see', value: step.see };
|
|
29
|
+
if (step.click) return { type: 'click', value: step.click };
|
|
30
|
+
if (step.wait) return { type: 'wait', ms: step.wait };
|
|
31
|
+
if (step.url) return { type: 'assert_url', value: step.url };
|
|
32
|
+
|
|
33
|
+
// 4. Color Assertions
|
|
34
|
+
if (step.color) {
|
|
35
|
+
const parts = step.color.split(/\s+on\s+/i);
|
|
36
|
+
if (parts.length === 2) {
|
|
37
|
+
return { type: 'assert_color', color: parts[0].trim(), element: parts[1].trim() };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (step.background) {
|
|
41
|
+
const parts = step.background.split(/\s+on\s+/i);
|
|
42
|
+
if (parts.length === 2) {
|
|
43
|
+
return { type: 'assert_background', color: parts[0].trim(), element: parts[1].trim() };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (step['border-color']) {
|
|
47
|
+
const parts = step['border-color'].split(/\s+on\s+/i);
|
|
48
|
+
if (parts.length === 2) {
|
|
49
|
+
return { type: 'assert_border_color', color: parts[0].trim(), element: parts[1].trim() };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 5. Network Syntax
|
|
54
|
+
if (step.network) {
|
|
55
|
+
const parts = step.network.split(' ');
|
|
56
|
+
return { type: 'network_listen', method: parts[0] || 'GET', urlPart: parts[1] || parts[0] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return step;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- 1.5. HELPER: Normalize Colors ---
|
|
63
|
+
function normalizeColor(color) {
|
|
64
|
+
// Convert any color format to RGB for comparison
|
|
65
|
+
const namedColors = {
|
|
66
|
+
'red': 'rgb(255, 0, 0)',
|
|
67
|
+
'blue': 'rgb(0, 0, 255)',
|
|
68
|
+
'green': 'rgb(0, 128, 0)',
|
|
69
|
+
'white': 'rgb(255, 255, 255)',
|
|
70
|
+
'black': 'rgb(0, 0, 0)',
|
|
71
|
+
'yellow': 'rgb(255, 255, 0)',
|
|
72
|
+
'gray': 'rgb(128, 128, 128)',
|
|
73
|
+
'grey': 'rgb(128, 128, 128)',
|
|
74
|
+
'orange': 'rgb(255, 165, 0)',
|
|
75
|
+
'purple': 'rgb(128, 0, 128)',
|
|
76
|
+
'pink': 'rgb(255, 192, 203)',
|
|
77
|
+
'brown': 'rgb(165, 42, 42)',
|
|
78
|
+
'cyan': 'rgb(0, 255, 255)',
|
|
79
|
+
'magenta': 'rgb(255, 0, 255)',
|
|
80
|
+
'lime': 'rgb(0, 255, 0)',
|
|
81
|
+
'navy': 'rgb(0, 0, 128)',
|
|
82
|
+
'teal': 'rgb(0, 128, 128)',
|
|
83
|
+
'silver': 'rgb(192, 192, 192)',
|
|
84
|
+
'gold': 'rgb(255, 215, 0)',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const trimmed = color.trim().toLowerCase();
|
|
88
|
+
|
|
89
|
+
// Handle named colors
|
|
90
|
+
if (namedColors[trimmed]) {
|
|
91
|
+
return namedColors[trimmed];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle hex colors (#fff, #ffffff, #ffffffff)
|
|
95
|
+
if (trimmed.startsWith('#')) {
|
|
96
|
+
let hex = trimmed.slice(1);
|
|
97
|
+
|
|
98
|
+
// Expand 3-digit hex to 6-digit
|
|
99
|
+
if (hex.length === 3) {
|
|
100
|
+
hex = hex.split('').map(c => c + c).join('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Parse RGB values
|
|
104
|
+
if (hex.length === 6 || hex.length === 8) {
|
|
105
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
106
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
107
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
108
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Already in rgb/rgba format - normalize spacing
|
|
113
|
+
if (trimmed.startsWith('rgb')) {
|
|
114
|
+
// Extract just rgb values, ignore alpha
|
|
115
|
+
const match = trimmed.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
116
|
+
if (match) {
|
|
117
|
+
return `rgb(${match[1]}, ${match[2]}, ${match[3]})`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return color;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- 2. HELPER: Wait for Server ---
|
|
125
|
+
function waitForServer(port = 3000, timeout = 30000) {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const start = Date.now();
|
|
128
|
+
const interval = setInterval(() => {
|
|
129
|
+
const socket = new net.Socket();
|
|
130
|
+
socket.setTimeout(1000);
|
|
131
|
+
socket.on('connect', () => { socket.destroy(); clearInterval(interval); resolve(); });
|
|
132
|
+
socket.on('error', () => {
|
|
133
|
+
socket.destroy();
|
|
134
|
+
if (Date.now() - start > timeout) {
|
|
135
|
+
clearInterval(interval);
|
|
136
|
+
reject(new Error('Server timeout'));
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
socket.connect(port, 'localhost');
|
|
140
|
+
}, 1000);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- 3. MAIN ENGINE ---
|
|
145
|
+
export async function runTests(page) {
|
|
146
|
+
console.log(chalk.bold.blue('\n🚀 Starting Vibe Verification Engine...\n'));
|
|
147
|
+
|
|
148
|
+
// A. Load YAML
|
|
149
|
+
const cwd = process.cwd();
|
|
150
|
+
const yamlPath = `${cwd}/vibe.yaml`;
|
|
151
|
+
let plan = "";
|
|
152
|
+
|
|
153
|
+
if (fs.existsSync(yamlPath)) {
|
|
154
|
+
try {
|
|
155
|
+
const fileContents = fs.readFileSync(yamlPath, 'utf8');
|
|
156
|
+
plan = yaml.load(fileContents);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.log(chalk.red(`❌ Error reading vibe.yaml: ${e.message}`));
|
|
159
|
+
return { success: false, error: "Error reading vibe.yaml" };
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
console.log(chalk.red(`❌ Could not find "vibe.yaml" in: ${cwd}`));
|
|
163
|
+
console.log(chalk.yellow(`💡 Make sure you're running this command from the project directory containing vibe.yaml`));
|
|
164
|
+
return { success: false, error: "Missing vibe.yaml" };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const baseUrl = plan.meta.baseUrl || 'http://localhost:3000';
|
|
168
|
+
const failureReport = []; // Store failures here
|
|
169
|
+
let totalTests = 0; // Track total number of tests
|
|
170
|
+
let passedTests = 0; // Track passed tests
|
|
171
|
+
|
|
172
|
+
// B. Smart Server Startup
|
|
173
|
+
let spinner = null; // Initialize spinner before any usage
|
|
174
|
+
try {
|
|
175
|
+
await page.goto(baseUrl, { waitUntil: 'networkidle', timeout: 3000 });
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.log(chalk.yellow('⚠️ Localhost is down. Auto-starting...'));
|
|
178
|
+
spinner = ora('Booting up server...').start();
|
|
179
|
+
const child = spawn('npm', ['run', 'dev'], { stdio: 'ignore', detached: true, shell: true });
|
|
180
|
+
child.unref();
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await waitForServer(3000);
|
|
184
|
+
spinner.succeed(chalk.green('Server is up!'));
|
|
185
|
+
spinner = null;
|
|
186
|
+
await page.goto(baseUrl, { waitUntil: 'networkidle' });
|
|
187
|
+
} catch (startError) {
|
|
188
|
+
spinner.fail(chalk.red('Could not auto-start server.'));
|
|
189
|
+
spinner = null;
|
|
190
|
+
return { success: false, error: "Server failed to start" };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// C. The Testing Loop
|
|
195
|
+
let pendingNetworkPromise = null;
|
|
196
|
+
|
|
197
|
+
for (const scenario of plan.scenarios) {
|
|
198
|
+
console.log(chalk.dim('-----------------------------------'));
|
|
199
|
+
console.log(chalk.bold.magenta(`Testing Scenario: ${scenario.name}`));
|
|
200
|
+
|
|
201
|
+
spinner = ora(`Navigating to ${scenario.path}...`).start();
|
|
202
|
+
try {
|
|
203
|
+
await page.goto(`${baseUrl}${scenario.path}`, { waitUntil: 'networkidle' });
|
|
204
|
+
spinner.succeed(`Arrived at ${scenario.path}`);
|
|
205
|
+
spinner = null;
|
|
206
|
+
} catch (e) {
|
|
207
|
+
spinner.fail(`Could not load ${scenario.path}`);
|
|
208
|
+
spinner = null;
|
|
209
|
+
failureReport.push({
|
|
210
|
+
scenario: scenario.name,
|
|
211
|
+
step: "Navigation",
|
|
212
|
+
error: `Could not load ${scenario.path}`
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const rawStep of scenario.tests) {
|
|
218
|
+
const action = normalizeAction(rawStep);
|
|
219
|
+
spinner = ora(`Checking...`).start();
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
// If we have a pending network listener, wait for it unless we are about to trigger it
|
|
223
|
+
if (pendingNetworkPromise && action.type !== 'click' && action.type !== 'type_smart') {
|
|
224
|
+
spinner.text = "Waiting for previous API call...";
|
|
225
|
+
await pendingNetworkPromise;
|
|
226
|
+
pendingNetworkPromise = null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Skip network_listen from test count as it's a setup action
|
|
230
|
+
if (action.type !== 'network_listen') {
|
|
231
|
+
totalTests++;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
switch (action.type) {
|
|
235
|
+
case 'see':
|
|
236
|
+
spinner.text = `Looking for text: "${action.value}"`;
|
|
237
|
+
if (await page.getByText(action.value).isVisible()) {
|
|
238
|
+
spinner.succeed(chalk.green(`✅ Saw: "${action.value}"`));
|
|
239
|
+
passedTests++;
|
|
240
|
+
} else {
|
|
241
|
+
throw new Error(`Text "${action.value}" not found.`);
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case 'assert_visible':
|
|
246
|
+
await page.waitForSelector(action.selector, { state: 'visible', timeout: 5000 });
|
|
247
|
+
spinner.succeed(chalk.green(`✅ Visible: ${action.selector}`));
|
|
248
|
+
passedTests++;
|
|
249
|
+
break;
|
|
250
|
+
|
|
251
|
+
case 'assert_text':
|
|
252
|
+
const el = await page.waitForSelector(action.selector);
|
|
253
|
+
const txt = await el.textContent();
|
|
254
|
+
if (txt.includes(action.value)) {
|
|
255
|
+
spinner.succeed(chalk.green(`✅ Text Match: "${action.value}"`));
|
|
256
|
+
passedTests++;
|
|
257
|
+
} else {
|
|
258
|
+
throw new Error(`Expected "${action.value}", found "${txt}"`);
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
case 'click':
|
|
263
|
+
spinner.text = `Clicking "${action.value}"...`;
|
|
264
|
+
const clickAction = page.click(`text=${action.value}`, { timeout: 5000 });
|
|
265
|
+
if (pendingNetworkPromise) {
|
|
266
|
+
await Promise.all([pendingNetworkPromise, clickAction]);
|
|
267
|
+
pendingNetworkPromise = null;
|
|
268
|
+
spinner.succeed(`🖱️ Clicked "${action.value}" & ✅ API Verified`);
|
|
269
|
+
} else {
|
|
270
|
+
await clickAction;
|
|
271
|
+
spinner.succeed(`🖱️ Clicked "${action.value}"`);
|
|
272
|
+
}
|
|
273
|
+
passedTests++;
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
case 'type_smart':
|
|
277
|
+
spinner.text = `Typing into "${action.label}"...`;
|
|
278
|
+
// Try Label first (best practice), then Placeholder (fallback)
|
|
279
|
+
const labelMatch = page.getByLabel(action.label);
|
|
280
|
+
const placeholderMatch = page.getByPlaceholder(action.label);
|
|
281
|
+
|
|
282
|
+
if (await labelMatch.count() > 0) await labelMatch.fill(action.value);
|
|
283
|
+
else if (await placeholderMatch.count() > 0) await placeholderMatch.fill(action.value);
|
|
284
|
+
else throw new Error(`Input "${action.label}" not found.`);
|
|
285
|
+
|
|
286
|
+
spinner.succeed(`⌨️ Typed "${action.value}"`);
|
|
287
|
+
passedTests++;
|
|
288
|
+
break;
|
|
289
|
+
|
|
290
|
+
case 'type_selector':
|
|
291
|
+
await page.fill(action.selector, action.value);
|
|
292
|
+
spinner.succeed(`⌨️ Typed into ${action.selector}`);
|
|
293
|
+
passedTests++;
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
case 'assert_url':
|
|
297
|
+
spinner.text = `Verifying URL...`;
|
|
298
|
+
await page.waitForURL(`**${action.value}**`, { timeout: 5000 });
|
|
299
|
+
spinner.succeed(`📍 Navigated to "${action.value}"`);
|
|
300
|
+
passedTests++;
|
|
301
|
+
break;
|
|
302
|
+
|
|
303
|
+
case 'network_listen':
|
|
304
|
+
spinner.text = `👂 Listening for ${action.method} ${action.urlPart}...`;
|
|
305
|
+
pendingNetworkPromise = page.waitForResponse(res =>
|
|
306
|
+
res.url().includes(action.urlPart) &&
|
|
307
|
+
res.request().method() === action.method &&
|
|
308
|
+
res.status() === 200
|
|
309
|
+
);
|
|
310
|
+
spinner.info(`👂 Listening for ${action.method} ${action.urlPart}...`);
|
|
311
|
+
break;
|
|
312
|
+
|
|
313
|
+
case 'assert_color':
|
|
314
|
+
spinner.text = `Checking text color of "${action.element}"...`;
|
|
315
|
+
try {
|
|
316
|
+
const colorElement = await page.getByText(action.element).first();
|
|
317
|
+
const actualColor = await colorElement.evaluate(el =>
|
|
318
|
+
window.getComputedStyle(el).color
|
|
319
|
+
);
|
|
320
|
+
const expectedColor = normalizeColor(action.color);
|
|
321
|
+
const normalizedActual = normalizeColor(actualColor);
|
|
322
|
+
|
|
323
|
+
if (normalizedActual === expectedColor) {
|
|
324
|
+
spinner.succeed(chalk.green(`✅ Color matches: ${action.color}`));
|
|
325
|
+
passedTests++;
|
|
326
|
+
} else {
|
|
327
|
+
throw new Error(`Expected color "${action.color}" (${expectedColor}) but got "${actualColor}" (${normalizedActual})`);
|
|
328
|
+
}
|
|
329
|
+
} catch (error) {
|
|
330
|
+
throw new Error(`Color check failed for "${action.element}": ${error.message}`);
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
|
|
334
|
+
case 'assert_background':
|
|
335
|
+
spinner.text = `Checking background color of "${action.element}"...`;
|
|
336
|
+
try {
|
|
337
|
+
const bgElement = await page.getByText(action.element).first();
|
|
338
|
+
const actualBg = await bgElement.evaluate(el =>
|
|
339
|
+
window.getComputedStyle(el).backgroundColor
|
|
340
|
+
);
|
|
341
|
+
const expectedBg = normalizeColor(action.color);
|
|
342
|
+
const normalizedBg = normalizeColor(actualBg);
|
|
343
|
+
|
|
344
|
+
if (normalizedBg === expectedBg) {
|
|
345
|
+
spinner.succeed(chalk.green(`✅ Background matches: ${action.color}`));
|
|
346
|
+
passedTests++;
|
|
347
|
+
} else {
|
|
348
|
+
throw new Error(`Expected background "${action.color}" (${expectedBg}) but got "${actualBg}" (${normalizedBg})`);
|
|
349
|
+
}
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw new Error(`Background check failed for "${action.element}": ${error.message}`);
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
|
|
355
|
+
case 'assert_border_color':
|
|
356
|
+
spinner.text = `Checking border color of "${action.element}"...`;
|
|
357
|
+
try {
|
|
358
|
+
const borderElement = await page.getByText(action.element).first();
|
|
359
|
+
const actualBorder = await borderElement.evaluate(el =>
|
|
360
|
+
window.getComputedStyle(el).borderColor
|
|
361
|
+
);
|
|
362
|
+
const expectedBorder = normalizeColor(action.color);
|
|
363
|
+
const normalizedBorder = normalizeColor(actualBorder);
|
|
364
|
+
|
|
365
|
+
if (normalizedBorder === expectedBorder) {
|
|
366
|
+
spinner.succeed(chalk.green(`✅ Border color matches: ${action.color}`));
|
|
367
|
+
passedTests++;
|
|
368
|
+
} else {
|
|
369
|
+
throw new Error(`Expected border color "${action.color}" (${expectedBorder}) but got "${actualBorder}" (${normalizedBorder})`);
|
|
370
|
+
}
|
|
371
|
+
} catch (error) {
|
|
372
|
+
throw new Error(`Border color check failed for "${action.element}": ${error.message}`);
|
|
373
|
+
}
|
|
374
|
+
break;
|
|
375
|
+
|
|
376
|
+
case 'wait':
|
|
377
|
+
await page.waitForTimeout(action.ms);
|
|
378
|
+
spinner.succeed(`zzz Waited ${action.ms}ms`);
|
|
379
|
+
passedTests++;
|
|
380
|
+
break;
|
|
381
|
+
|
|
382
|
+
default:
|
|
383
|
+
spinner.warn(`⚠️ Unknown Action: ${JSON.stringify(action)}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Clean up spinner after successful test step
|
|
387
|
+
spinner = null;
|
|
388
|
+
|
|
389
|
+
} catch (error) {
|
|
390
|
+
spinner.fail(chalk.red(`❌ FAILED: ${JSON.stringify(action)}`));
|
|
391
|
+
spinner = null;
|
|
392
|
+
failureReport.push({
|
|
393
|
+
scenario: scenario.name,
|
|
394
|
+
action: action,
|
|
395
|
+
error: error.message
|
|
396
|
+
});
|
|
397
|
+
pendingNetworkPromise = null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
console.log(chalk.dim('\n-----------------------------------'));
|
|
403
|
+
|
|
404
|
+
// Check if tests actually ran
|
|
405
|
+
if (totalTests === 0) {
|
|
406
|
+
console.log(chalk.yellow.bold('⚠️ No tests were executed. Please check your vibe.yaml file.'));
|
|
407
|
+
return { success: false, error: "No tests executed" };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (failureReport.length === 0 && passedTests === totalTests) {
|
|
411
|
+
console.log(chalk.green.bold(`✨ All Vibes Passed! ${passedTests}/${totalTests} tests successful.`));
|
|
412
|
+
return { success: true, totalTests, passedTests };
|
|
413
|
+
} else {
|
|
414
|
+
console.log(chalk.red.bold(`🚫 Vibe Check Failed! ${failureReport.length} failures out of ${totalTests} tests.`));
|
|
415
|
+
console.log(chalk.yellow(` Passed: ${passedTests}/${totalTests}`));
|
|
416
|
+
return { success: false, failures: failureReport, totalTests, passedTests };
|
|
417
|
+
}
|
|
418
|
+
}
|
package/lib/scan.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// NOT USED ANYWHERE YET. NEEDS MORE REFINEMENT.
|
|
2
|
+
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import exec from "child_process";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import { stdout } from "process";
|
|
7
|
+
|
|
8
|
+
async function scanBuildErrors() {
|
|
9
|
+
console.log(chalk.green('🌊 Vibe Guard Initialized...'));
|
|
10
|
+
|
|
11
|
+
// Start the spinner (The event loop needs to be free for this to spin!)
|
|
12
|
+
const spinner = ora(chalk.yellow('👀 Looking for Hallucinations (Running build)...')).start();
|
|
13
|
+
let result = ""
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// We use 'await' here. This allows the spinner to keep spinning
|
|
17
|
+
// while the build runs in the background.
|
|
18
|
+
const { stdout, stderr } = exec.exec("npm run build", {
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
maxBuffer: 50 * 1024 * 1024
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// If we get here, the exit code was 0 (Success)
|
|
24
|
+
spinner.succeed(chalk.green('Build Passed! No errors found.'));
|
|
25
|
+
result = stdout + "\n" + stderr;
|
|
26
|
+
return result;
|
|
27
|
+
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// If 'npm run build' fails (exit code 1), it throws an error.
|
|
30
|
+
// We catch it here.
|
|
31
|
+
spinner.fail(chalk.red('Build Failed! Hallucinations detected.'));
|
|
32
|
+
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(chalk.dim('--- Error Logs ---'));
|
|
35
|
+
|
|
36
|
+
// In the catch block, the output is inside error.stdout / error.stderr
|
|
37
|
+
const logs = (error.stdout || "") + "\n" + (error.stderr || "");
|
|
38
|
+
console.log(error);
|
|
39
|
+
// result = stdout + "\n" + stderr;
|
|
40
|
+
return logs;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// function (error, stdout, stderr) {
|
|
44
|
+
// if (error) {
|
|
45
|
+
// spinner.fail(chalk.red('Build Failed! Hallucinations detected.'));
|
|
46
|
+
// console.log('');
|
|
47
|
+
// console.log(chalk.dim('--- Error Logs ---'));
|
|
48
|
+
// console.log(stdout);
|
|
49
|
+
// console.log(stderr);
|
|
50
|
+
// return stdout + "\n" + stderr;
|
|
51
|
+
// } else {
|
|
52
|
+
// spinner.succeed(chalk.green('Build Passed! No errors found.'));
|
|
53
|
+
// return "";
|
|
54
|
+
// }
|
|
55
|
+
// }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { scanBuildErrors };
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vatzzza/botintern",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"module": "index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"botintern": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@types/bun": "latest"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"typescript": "^5"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@google/genai": "^1.38.0",
|
|
17
|
+
"@google/generative-ai": "^0.24.1",
|
|
18
|
+
"chalk": "^5.6.2",
|
|
19
|
+
"commander": "^14.0.2",
|
|
20
|
+
"dotenv": "^17.2.3",
|
|
21
|
+
"js-yaml": "^4.1.1",
|
|
22
|
+
"ora": "^9.1.0",
|
|
23
|
+
"playwright": "^1.58.0",
|
|
24
|
+
"yocto-spinner": "^1.0.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "bun --watch index.js"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// lib/utils.js
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Removes weird terminal colors (ANSI codes) so Regex works
|
|
6
|
+
*/
|
|
7
|
+
function stripAnsi(string) {
|
|
8
|
+
return string.replace(
|
|
9
|
+
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
|
|
10
|
+
''
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Hunts through the logs to find the first broken file.
|
|
16
|
+
* Returns null if no file is found.
|
|
17
|
+
*/
|
|
18
|
+
export function extractErrorFile(logs) {
|
|
19
|
+
const cleanLogs = stripAnsi(logs);
|
|
20
|
+
|
|
21
|
+
// REGEX: Looks for paths starting with ./ followed by common folders
|
|
22
|
+
// Captures: ./app/page.tsx, ./src/components/Button.tsx, etc.
|
|
23
|
+
const regex = /(\.\/(?:app|src|components|pages|lib)\/[a-zA-Z0-9_\-\/]+\.(tsx|ts|jsx|js))/i;
|
|
24
|
+
|
|
25
|
+
const match = cleanLogs.match(regex);
|
|
26
|
+
|
|
27
|
+
if (match && match[1]) {
|
|
28
|
+
// Return the clean relative path (e.g., "./app/page.tsx")
|
|
29
|
+
return match[1];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import exec from "child_process";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
|
|
5
|
+
function setupPlaywright() {
|
|
6
|
+
const spinner = ora(chalk.green('Installing @playwright/test...')).start();
|
|
7
|
+
exec.exec("npm i playwright", function (error, stdout, stderr) {
|
|
8
|
+
console.log(stdout);
|
|
9
|
+
console.log(stderr);
|
|
10
|
+
if (error) {
|
|
11
|
+
spinner.fail(chalk.red('Installing @playwright/test...'));
|
|
12
|
+
console.error(error);
|
|
13
|
+
} else {
|
|
14
|
+
spinner.succeed(chalk.green('Installing @playwright/test...'));
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
exec.exec("npx playwright install", function (error, stdout, stderr) {
|
|
18
|
+
console.log(stdout);
|
|
19
|
+
console.log(stderr);
|
|
20
|
+
if (error) {
|
|
21
|
+
spinner.fail(chalk.red('Installing playwright...'));
|
|
22
|
+
console.error(error);
|
|
23
|
+
} else {
|
|
24
|
+
spinner.succeed(chalk.green('Installing playwright...'));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { setupPlaywright };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
meta:
|
|
2
|
+
appName: "My Next.js App"
|
|
3
|
+
baseUrl: "http://localhost:3000"
|
|
4
|
+
|
|
5
|
+
scenarios:
|
|
6
|
+
- name: "About Page Check"
|
|
7
|
+
path: "/about"
|
|
8
|
+
tests:
|
|
9
|
+
# 1. Check if we actually landed on the page (Look for the main header)
|
|
10
|
+
- name: "Check if we actually landed on the page (Look for the main header)"
|
|
11
|
+
type: "assert_visible"
|
|
12
|
+
selector: "h1"
|
|
13
|
+
|
|
14
|
+
# 2. strict check: The h1 should say "About"
|
|
15
|
+
- type: "assert_text"
|
|
16
|
+
selector: "h1"
|
|
17
|
+
value: "About"
|
|
18
|
+
|
|
19
|
+
# 3. (Optional) Check for other common elements like a nav bar
|
|
20
|
+
- type: "assert_visible"
|
|
21
|
+
selector: "nav"
|