intellitester 0.2.50 → 0.2.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-WNSVKXTD.js → chunk-PGOHFXEG.js} +835 -792
- package/dist/chunk-PGOHFXEG.js.map +1 -0
- package/dist/{chunk-MVWYKWHN.cjs → chunk-ZXCFOTWX.cjs} +835 -792
- package/dist/chunk-ZXCFOTWX.cjs.map +1 -0
- package/dist/cli/index.cjs +121 -54
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +108 -41
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +11 -11
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-MVWYKWHN.cjs.map +0 -1
- package/dist/chunk-WNSVKXTD.js.map +0 -1
|
@@ -4,6 +4,7 @@ import { loadCleanupHandlers, executeCleanup, saveFailedCleanup } from './chunk-
|
|
|
4
4
|
import { NumberDictionary, uniqueNamesGenerator, adjectives, animals } from 'unique-names-generator';
|
|
5
5
|
import crypto4 from 'crypto';
|
|
6
6
|
import { parsePhoneNumber, isValidPhoneNumber } from 'libphonenumber-js';
|
|
7
|
+
import { createServer } from 'http';
|
|
7
8
|
import * as fs from 'fs/promises';
|
|
8
9
|
import fs__default from 'fs/promises';
|
|
9
10
|
import * as path4 from 'path';
|
|
@@ -14,7 +15,6 @@ import { Client, Users, TablesDB, Storage, Teams } from 'node-appwrite';
|
|
|
14
15
|
import { Anthropic } from '@llamaindex/anthropic';
|
|
15
16
|
import { OpenAI } from '@llamaindex/openai';
|
|
16
17
|
import { Ollama } from '@llamaindex/ollama';
|
|
17
|
-
import { createServer } from 'http';
|
|
18
18
|
import { spawn } from 'child_process';
|
|
19
19
|
import { rmSync } from 'fs';
|
|
20
20
|
|
|
@@ -297,791 +297,791 @@ function interpolateVariables(value, variables) {
|
|
|
297
297
|
}
|
|
298
298
|
});
|
|
299
299
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
this.
|
|
300
|
+
var TrackingServer = class {
|
|
301
|
+
constructor() {
|
|
302
|
+
this.server = null;
|
|
303
|
+
this.resources = /* @__PURE__ */ new Map();
|
|
304
|
+
this.port = 0;
|
|
305
305
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
306
|
+
async start(options = {}) {
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
this.server = createServer((req, res) => {
|
|
309
|
+
this.handleRequest(req, res);
|
|
310
|
+
});
|
|
311
|
+
this.server.on("error", (error) => {
|
|
312
|
+
reject(error);
|
|
313
|
+
});
|
|
314
|
+
const port = options.port ?? 0;
|
|
315
|
+
this.server.listen(port, () => {
|
|
316
|
+
const address = this.server?.address();
|
|
317
|
+
if (address && typeof address === "object") {
|
|
318
|
+
this.port = address.port;
|
|
319
|
+
resolve();
|
|
320
|
+
} else {
|
|
321
|
+
reject(new Error("Failed to get server port"));
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
});
|
|
311
325
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
throw new Error(
|
|
321
|
-
`Failed to list messages for ${email}: ${response.status} ${response.statusText}`
|
|
322
|
-
);
|
|
326
|
+
handleRequest(req, res) {
|
|
327
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
328
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
329
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
330
|
+
if (req.method === "OPTIONS") {
|
|
331
|
+
res.writeHead(204);
|
|
332
|
+
res.end();
|
|
333
|
+
return;
|
|
323
334
|
}
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const url = `${this.endpoint}/api/v1/mailbox/${mailbox}/${id}`;
|
|
333
|
-
const response = await fetch(url);
|
|
334
|
-
if (!response.ok) {
|
|
335
|
-
throw new Error(
|
|
336
|
-
`Failed to get message ${id} for ${email}: ${response.status} ${response.statusText}`
|
|
337
|
-
);
|
|
335
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
336
|
+
if (req.method === "POST" && url.pathname === "/track") {
|
|
337
|
+
this.handleTrackRequest(req, res);
|
|
338
|
+
} else if (req.method === "GET" && url.pathname.startsWith("/resources/")) {
|
|
339
|
+
this.handleGetResources(url, res);
|
|
340
|
+
} else {
|
|
341
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
342
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
338
343
|
}
|
|
339
|
-
const message = await response.json();
|
|
340
|
-
return message;
|
|
341
344
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return msg.subject.includes(subjectContains);
|
|
345
|
+
handleTrackRequest(req, res) {
|
|
346
|
+
let body = "";
|
|
347
|
+
req.on("data", (chunk) => {
|
|
348
|
+
body += chunk.toString();
|
|
349
|
+
});
|
|
350
|
+
req.on("end", () => {
|
|
351
|
+
try {
|
|
352
|
+
const trackRequest = JSON.parse(body);
|
|
353
|
+
if (!trackRequest.sessionId || !trackRequest.type || !trackRequest.id) {
|
|
354
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
355
|
+
res.end(JSON.stringify({ error: "Missing required fields (sessionId, type, id)" }));
|
|
356
|
+
return;
|
|
355
357
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
358
|
+
const { sessionId, ...resourceData } = trackRequest;
|
|
359
|
+
const resource = {
|
|
360
|
+
...resourceData,
|
|
361
|
+
type: trackRequest.type,
|
|
362
|
+
id: trackRequest.id,
|
|
363
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
364
|
+
};
|
|
365
|
+
const sessionResources = this.resources.get(sessionId) || [];
|
|
366
|
+
sessionResources.push(resource);
|
|
367
|
+
this.resources.set(sessionId, sessionResources);
|
|
368
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
369
|
+
res.end(JSON.stringify({ success: true }));
|
|
370
|
+
} catch {
|
|
371
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
372
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
360
373
|
}
|
|
361
|
-
|
|
374
|
+
});
|
|
375
|
+
req.on("error", () => {
|
|
376
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
377
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
handleGetResources(url, res) {
|
|
381
|
+
const sessionId = url.pathname.split("/").pop();
|
|
382
|
+
if (!sessionId) {
|
|
383
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
384
|
+
res.end(JSON.stringify({ error: "Missing sessionId" }));
|
|
385
|
+
return;
|
|
362
386
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
);
|
|
387
|
+
const resources = this.resources.get(sessionId) || [];
|
|
388
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
389
|
+
res.end(JSON.stringify({ resources }));
|
|
366
390
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
*/
|
|
370
|
-
extractCode(email, pattern) {
|
|
371
|
-
const regex = pattern ?? /\b(\d{6})\b/;
|
|
372
|
-
const text = email.body.text || email.body.html;
|
|
373
|
-
const match = text.match(regex);
|
|
374
|
-
return match ? match[1] : null;
|
|
391
|
+
getResources(sessionId) {
|
|
392
|
+
return this.resources.get(sessionId) ?? [];
|
|
375
393
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
*/
|
|
379
|
-
extractLink(email, pattern) {
|
|
380
|
-
const regex = pattern ?? /https?:\/\/[^\s"'<>]+/;
|
|
381
|
-
const text = email.body.text || email.body.html;
|
|
382
|
-
const match = text.match(regex);
|
|
383
|
-
return match ? match[0] : null;
|
|
394
|
+
clearSession(sessionId) {
|
|
395
|
+
this.resources.delete(sessionId);
|
|
384
396
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
397
|
+
async stop() {
|
|
398
|
+
return new Promise((resolve, reject) => {
|
|
399
|
+
if (!this.server) {
|
|
400
|
+
resolve();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
this.server.close((error) => {
|
|
404
|
+
if (error) {
|
|
405
|
+
reject(error);
|
|
406
|
+
} else {
|
|
407
|
+
this.server = null;
|
|
408
|
+
resolve();
|
|
409
|
+
}
|
|
410
|
+
});
|
|
393
411
|
});
|
|
394
|
-
if (!response.ok) {
|
|
395
|
-
throw new Error(
|
|
396
|
-
`Failed to delete message ${id} for ${email}: ${response.status} ${response.statusText}`
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
412
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
+
};
|
|
414
|
+
async function startTrackingServer(options) {
|
|
415
|
+
const server = new TrackingServer();
|
|
416
|
+
await server.start(options);
|
|
417
|
+
return server;
|
|
418
|
+
}
|
|
419
|
+
var TRACK_DIR = ".intellitester/track";
|
|
420
|
+
var ACTIVE_TESTS_FILE = "ACTIVE_TESTS.json";
|
|
421
|
+
var DEFAULT_STALE_MS = 2 * 60 * 60 * 1e3;
|
|
422
|
+
var HEARTBEAT_MS = 15e3;
|
|
423
|
+
var getTrackDir = (cwd, trackDir) => trackDir ? path4__default.resolve(cwd, trackDir) : path4__default.join(cwd, TRACK_DIR);
|
|
424
|
+
var getActiveTestsPath = (cwd, trackDir) => path4__default.join(getTrackDir(cwd, trackDir), ACTIVE_TESTS_FILE);
|
|
425
|
+
var loadActiveTests = async (cwd, trackDir) => {
|
|
426
|
+
const filePath = getActiveTestsPath(cwd, trackDir);
|
|
427
|
+
try {
|
|
428
|
+
const content = await fs__default.readFile(filePath, "utf8");
|
|
429
|
+
const parsed = JSON.parse(content);
|
|
430
|
+
if (!parsed.sessions || typeof parsed.sessions !== "object") {
|
|
431
|
+
throw new Error("Invalid ACTIVE_TESTS structure");
|
|
413
432
|
}
|
|
433
|
+
return parsed;
|
|
434
|
+
} catch {
|
|
435
|
+
return {
|
|
436
|
+
version: 1,
|
|
437
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
438
|
+
sessions: {}
|
|
439
|
+
};
|
|
414
440
|
}
|
|
415
441
|
};
|
|
416
|
-
var
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
this.users = new Users(this.client);
|
|
421
|
-
this.tablesDB = new TablesDB(this.client);
|
|
422
|
-
this.storage = new Storage(this.client);
|
|
423
|
-
this.teams = new Teams(this.client);
|
|
424
|
-
}
|
|
425
|
-
async cleanup(context, sessionId, cwd) {
|
|
426
|
-
const deleted = [];
|
|
427
|
-
const failed = [];
|
|
428
|
-
const sortedResources = [...context.resources].reverse();
|
|
429
|
-
for (const resource of sortedResources) {
|
|
430
|
-
if (resource.deleted) {
|
|
431
|
-
deleted.push(`${resource.type}:${resource.id} (already deleted)`);
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
try {
|
|
435
|
-
switch (resource.type) {
|
|
436
|
-
case "row":
|
|
437
|
-
if (resource.databaseId && resource.tableId) {
|
|
438
|
-
await this.tablesDB.deleteRow({
|
|
439
|
-
databaseId: resource.databaseId,
|
|
440
|
-
tableId: resource.tableId,
|
|
441
|
-
rowId: resource.id
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
break;
|
|
445
|
-
case "file":
|
|
446
|
-
if (resource.bucketId) {
|
|
447
|
-
await this.storage.deleteFile(resource.bucketId, resource.id);
|
|
448
|
-
}
|
|
449
|
-
break;
|
|
450
|
-
case "membership":
|
|
451
|
-
if (resource.teamId) {
|
|
452
|
-
await this.teams.deleteMembership(resource.teamId, resource.id);
|
|
453
|
-
}
|
|
454
|
-
break;
|
|
455
|
-
case "team":
|
|
456
|
-
await this.teams.delete(resource.id);
|
|
457
|
-
break;
|
|
458
|
-
case "message":
|
|
459
|
-
deleted.push(`${resource.type}:${resource.id} (skipped - messages cannot be deleted)`);
|
|
460
|
-
continue;
|
|
461
|
-
case "user":
|
|
462
|
-
break;
|
|
463
|
-
}
|
|
464
|
-
deleted.push(`${resource.type}:${resource.id}`);
|
|
465
|
-
} catch (error) {
|
|
466
|
-
failed.push(`${resource.type}:${resource.id}`);
|
|
467
|
-
console.warn(`Failed to delete ${resource.type} ${resource.id}:`, error);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
if (context.userId) {
|
|
471
|
-
try {
|
|
472
|
-
await this.users.delete(context.userId);
|
|
473
|
-
deleted.push(`user:${context.userId}`);
|
|
474
|
-
} catch (error) {
|
|
475
|
-
failed.push(`user:${context.userId}`);
|
|
476
|
-
console.warn(`Failed to delete user ${context.userId}:`, error);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
if (failed.length > 0 && sessionId) {
|
|
480
|
-
const failedResources = context.resources.filter(
|
|
481
|
-
(r) => failed.some((f) => f.includes(r.id))
|
|
482
|
-
);
|
|
483
|
-
await saveFailedCleanup({
|
|
484
|
-
sessionId,
|
|
485
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
486
|
-
resources: failedResources.map((r) => ({
|
|
487
|
-
type: r.type,
|
|
488
|
-
id: r.id,
|
|
489
|
-
databaseId: r.databaseId,
|
|
490
|
-
tableId: r.tableId,
|
|
491
|
-
bucketId: r.bucketId,
|
|
492
|
-
teamId: r.teamId
|
|
493
|
-
})),
|
|
494
|
-
providerConfig: {
|
|
495
|
-
provider: "appwrite",
|
|
496
|
-
endpoint: this.config.endpoint,
|
|
497
|
-
projectId: this.config.projectId
|
|
498
|
-
},
|
|
499
|
-
errors: failed
|
|
500
|
-
}, cwd);
|
|
501
|
-
}
|
|
502
|
-
return { success: failed.length === 0, deleted, failed };
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
function createTestContext() {
|
|
506
|
-
return {
|
|
507
|
-
resources: [],
|
|
508
|
-
variables: /* @__PURE__ */ new Map()
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// src/integrations/appwrite/types.ts
|
|
513
|
-
var APPWRITE_PATTERNS = {
|
|
514
|
-
userCreate: /\/v1\/account$/,
|
|
515
|
-
rowCreate: /\/v1\/tablesdb\/([\w-]+)\/tables\/([\w-]+)\/rows$/,
|
|
516
|
-
fileCreate: /\/v1\/storage\/buckets\/([\w-]+)\/files$/,
|
|
517
|
-
teamCreate: /\/v1\/teams$/,
|
|
518
|
-
membershipCreate: /\/v1\/teams\/([\w-]+)\/memberships$/,
|
|
519
|
-
messageCreate: /\/v1\/messaging\/messages$/
|
|
520
|
-
};
|
|
521
|
-
var APPWRITE_UPDATE_PATTERNS = {
|
|
522
|
-
rowUpdate: /\/v1\/tablesdb\/([\w-]+)\/tables\/([\w-]+)\/rows\/([\w-]+)$/,
|
|
523
|
-
fileUpdate: /\/v1\/storage\/buckets\/([\w-]+)\/files\/([\w-]+)$/,
|
|
524
|
-
teamUpdate: /\/v1\/teams\/([\w-]+)$/
|
|
525
|
-
};
|
|
526
|
-
var APPWRITE_DELETE_PATTERNS = {
|
|
527
|
-
rowDelete: /\/v1\/tablesdb\/([\w-]+)\/tables\/([\w-]+)\/rows\/([\w-]+)$/,
|
|
528
|
-
fileDelete: /\/v1\/storage\/buckets\/([\w-]+)\/files\/([\w-]+)$/,
|
|
529
|
-
teamDelete: /\/v1\/teams\/([\w-]+)$/,
|
|
530
|
-
membershipDelete: /\/v1\/teams\/([\w-]+)\/memberships\/([\w-]+)$/
|
|
442
|
+
var saveActiveTests = async (cwd, state, trackDir) => {
|
|
443
|
+
const filePath = getActiveTestsPath(cwd, trackDir);
|
|
444
|
+
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
445
|
+
await fs__default.writeFile(filePath, JSON.stringify(state, null, 2), "utf8");
|
|
531
446
|
};
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
// Mobile portrait
|
|
537
|
-
sm: { width: 640, height: 800 },
|
|
538
|
-
// Small tablet
|
|
539
|
-
md: { width: 768, height: 1024 },
|
|
540
|
-
// Tablet
|
|
541
|
-
lg: { width: 1024, height: 768 },
|
|
542
|
-
// Desktop
|
|
543
|
-
xl: { width: 1280, height: 720 }
|
|
544
|
-
// Large desktop
|
|
447
|
+
var isStale = (entry, staleMs) => {
|
|
448
|
+
const last = new Date(entry.lastUpdated).getTime();
|
|
449
|
+
if (Number.isNaN(last)) return true;
|
|
450
|
+
return Date.now() - last > staleMs;
|
|
545
451
|
};
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
return { width, height };
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
function getBrowserLaunchOptions(options) {
|
|
561
|
-
const { headless, browser } = options;
|
|
562
|
-
if (browser === "chromium" || !browser) {
|
|
563
|
-
return {
|
|
564
|
-
headless,
|
|
565
|
-
args: [
|
|
566
|
-
// Use Chrome's new headless mode which behaves more like regular browser
|
|
567
|
-
...headless ? ["--headless=new"] : [],
|
|
568
|
-
// For Docker/CI environments - disables sandboxing (required for root/container)
|
|
569
|
-
"--no-sandbox",
|
|
570
|
-
// Shared memory - critical for Docker/CI, harmless locally
|
|
571
|
-
"--disable-dev-shm-usage",
|
|
572
|
-
// GPU acceleration - not needed in headless mode
|
|
573
|
-
"--disable-gpu",
|
|
574
|
-
// Set explicit window size (fallback for viewport)
|
|
575
|
-
"--window-size=1920,1080",
|
|
576
|
-
// Reduce overhead
|
|
577
|
-
"--disable-extensions",
|
|
578
|
-
// Prevent JavaScript throttling (helps with slow page loads)
|
|
579
|
-
"--disable-background-timer-throttling",
|
|
580
|
-
"--disable-backgrounding-occluded-windows",
|
|
581
|
-
"--disable-renderer-backgrounding",
|
|
582
|
-
// Process isolation - reduces overhead for testing
|
|
583
|
-
"--disable-features=IsolateOrigins,site-per-process",
|
|
584
|
-
// Networking tweaks
|
|
585
|
-
"--disable-features=VizDisplayCompositor",
|
|
586
|
-
"--disable-blink-features=AutomationControlled"
|
|
587
|
-
]
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
if (browser === "firefox") {
|
|
591
|
-
return {
|
|
592
|
-
headless,
|
|
593
|
-
firefoxUserPrefs: {
|
|
594
|
-
// Enable JIT (disabled in automation mode by default, causes 2-4x JS slowdown)
|
|
595
|
-
"javascript.options.jit": true,
|
|
596
|
-
// Disable fission for stability/speed
|
|
597
|
-
"fission.autostart": false,
|
|
598
|
-
// Disable hardware acceleration overhead in headless
|
|
599
|
-
"layers.acceleration.disabled": true,
|
|
600
|
-
"gfx.webrender.all": false
|
|
452
|
+
var readTrackedResources = async (trackFile) => {
|
|
453
|
+
try {
|
|
454
|
+
const content = await fs__default.readFile(trackFile, "utf8");
|
|
455
|
+
if (!content.trim()) return [];
|
|
456
|
+
return content.split("\n").filter(Boolean).map((line) => {
|
|
457
|
+
try {
|
|
458
|
+
return JSON.parse(line);
|
|
459
|
+
} catch {
|
|
460
|
+
return null;
|
|
601
461
|
}
|
|
602
|
-
};
|
|
462
|
+
}).filter((entry) => Boolean(entry));
|
|
463
|
+
} catch {
|
|
464
|
+
return [];
|
|
603
465
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
466
|
+
};
|
|
467
|
+
var cleanupStaleSession = async (entry, cleanupConfig) => {
|
|
468
|
+
if (!entry.providerConfig || !cleanupConfig?.provider) return;
|
|
469
|
+
const resources = await readTrackedResources(entry.trackFile);
|
|
470
|
+
if (resources.length === 0) return;
|
|
471
|
+
try {
|
|
472
|
+
const { handlers, typeMappings, provider } = await loadCleanupHandlers(cleanupConfig, entry.cwd);
|
|
473
|
+
if (!provider) return;
|
|
474
|
+
await executeCleanup(resources, handlers, typeMappings, {
|
|
475
|
+
parallel: cleanupConfig.parallel ?? false,
|
|
476
|
+
retries: cleanupConfig.retries ?? 3,
|
|
477
|
+
sessionId: entry.sessionId,
|
|
478
|
+
testStartTime: entry.startedAt,
|
|
479
|
+
providerConfig: entry.providerConfig,
|
|
480
|
+
cwd: entry.cwd,
|
|
481
|
+
config: { ...cleanupConfig, scanUntracked: false },
|
|
482
|
+
provider
|
|
617
483
|
});
|
|
484
|
+
} catch {
|
|
618
485
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
486
|
+
};
|
|
487
|
+
var cleanupOrphanedTrackFiles = async (cwd, state, trackDir) => {
|
|
488
|
+
const dir = getTrackDir(cwd, trackDir);
|
|
489
|
+
try {
|
|
490
|
+
const files = await fs__default.readdir(dir);
|
|
491
|
+
const activeFiles = new Set(Object.values(state.sessions).map((s) => path4__default.basename(s.trackFile)));
|
|
492
|
+
for (const file of files) {
|
|
493
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
494
|
+
if (activeFiles.has(file)) continue;
|
|
495
|
+
const filePath = path4__default.join(dir, file);
|
|
496
|
+
try {
|
|
497
|
+
const stat2 = await fs__default.stat(filePath);
|
|
498
|
+
const staleMs = Number(process.env.INTELLITESTER_STALE_TEST_MS ?? DEFAULT_STALE_MS);
|
|
499
|
+
const isOld = Date.now() - stat2.mtimeMs > staleMs;
|
|
500
|
+
const isEmpty = stat2.size === 0;
|
|
501
|
+
if (isEmpty || isOld) {
|
|
502
|
+
await fs__default.rm(filePath, { force: true });
|
|
503
|
+
}
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
629
506
|
}
|
|
630
|
-
|
|
507
|
+
} catch {
|
|
631
508
|
}
|
|
632
509
|
};
|
|
633
|
-
var
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
async generateCompletion(prompt, systemPrompt) {
|
|
646
|
-
const messages = [];
|
|
647
|
-
if (systemPrompt) {
|
|
648
|
-
messages.push({ role: "system", content: systemPrompt });
|
|
510
|
+
var pruneStaleTests = async (cwd, cleanupConfig, trackDir) => {
|
|
511
|
+
const state = await loadActiveTests(cwd, trackDir);
|
|
512
|
+
const staleMs = Number(process.env.INTELLITESTER_STALE_TEST_MS ?? DEFAULT_STALE_MS);
|
|
513
|
+
let changed = false;
|
|
514
|
+
for (const [sessionId, entry] of Object.entries(state.sessions)) {
|
|
515
|
+
let missingFile = false;
|
|
516
|
+
let isEmpty = false;
|
|
517
|
+
try {
|
|
518
|
+
const stat2 = await fs__default.stat(entry.trackFile);
|
|
519
|
+
isEmpty = stat2.size === 0;
|
|
520
|
+
} catch {
|
|
521
|
+
missingFile = true;
|
|
649
522
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
523
|
+
if (!missingFile && !isEmpty && !isStale(entry, staleMs)) continue;
|
|
524
|
+
changed = true;
|
|
525
|
+
await cleanupStaleSession(entry, cleanupConfig);
|
|
526
|
+
try {
|
|
527
|
+
await fs__default.rm(entry.trackFile, { force: true });
|
|
528
|
+
} catch {
|
|
655
529
|
}
|
|
656
|
-
|
|
530
|
+
delete state.sessions[sessionId];
|
|
657
531
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
constructor(config) {
|
|
661
|
-
this.config = config;
|
|
662
|
-
this.client = new Ollama({
|
|
663
|
-
model: this.config.model,
|
|
664
|
-
options: {
|
|
665
|
-
temperature: this.config.temperature
|
|
666
|
-
}
|
|
667
|
-
});
|
|
532
|
+
if (changed) {
|
|
533
|
+
await saveActiveTests(cwd, state, trackDir);
|
|
668
534
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
535
|
+
await cleanupOrphanedTrackFiles(cwd, state, trackDir);
|
|
536
|
+
};
|
|
537
|
+
async function initFileTracking(options) {
|
|
538
|
+
const cwd = options.cwd ?? process.cwd();
|
|
539
|
+
const trackDir = options.trackDir;
|
|
540
|
+
await fs__default.mkdir(getTrackDir(cwd, trackDir), { recursive: true });
|
|
541
|
+
await pruneStaleTests(cwd, options.cleanupConfig, trackDir);
|
|
542
|
+
const trackFile = path4__default.join(getTrackDir(cwd, trackDir), `TEST_SESSION_${options.sessionId}.jsonl`);
|
|
543
|
+
await fs__default.writeFile(trackFile, "", "utf8");
|
|
544
|
+
const state = await loadActiveTests(cwd, trackDir);
|
|
545
|
+
state.sessions[options.sessionId] = {
|
|
546
|
+
sessionId: options.sessionId,
|
|
547
|
+
trackFile,
|
|
548
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
549
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
550
|
+
cwd,
|
|
551
|
+
providerConfig: options.providerConfig
|
|
552
|
+
};
|
|
553
|
+
await saveActiveTests(cwd, state, trackDir);
|
|
554
|
+
const touch = async () => {
|
|
555
|
+
const current = await loadActiveTests(cwd, trackDir);
|
|
556
|
+
const entry = current.sessions[options.sessionId];
|
|
557
|
+
if (!entry) return;
|
|
558
|
+
entry.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
559
|
+
current.sessions[options.sessionId] = entry;
|
|
560
|
+
await saveActiveTests(cwd, current, trackDir);
|
|
561
|
+
};
|
|
562
|
+
const interval = setInterval(() => {
|
|
563
|
+
void touch();
|
|
564
|
+
}, HEARTBEAT_MS);
|
|
565
|
+
const stop = async () => {
|
|
566
|
+
clearInterval(interval);
|
|
567
|
+
const current = await loadActiveTests(cwd, trackDir);
|
|
568
|
+
delete current.sessions[options.sessionId];
|
|
569
|
+
await saveActiveTests(cwd, current, trackDir);
|
|
570
|
+
try {
|
|
571
|
+
await fs__default.rm(trackFile, { force: true });
|
|
572
|
+
} catch {
|
|
673
573
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
574
|
+
};
|
|
575
|
+
return { trackFile, stop };
|
|
576
|
+
}
|
|
577
|
+
async function mergeFileTrackedResources(trackFile, target, allowedTypes) {
|
|
578
|
+
const entries = await readTrackedResources(trackFile);
|
|
579
|
+
for (const entry of entries) {
|
|
580
|
+
const resource = entry;
|
|
581
|
+
if (!resource.type || !resource.id) continue;
|
|
582
|
+
if (allowedTypes && !allowedTypes.has(String(resource.type))) continue;
|
|
583
|
+
const exists = target.resources.some(
|
|
584
|
+
(existing) => existing.type === resource.type && existing.id === resource.id
|
|
585
|
+
);
|
|
586
|
+
if (!exists) {
|
|
587
|
+
target.resources.push(resource);
|
|
588
|
+
}
|
|
589
|
+
if (resource.type === "user") {
|
|
590
|
+
if (!target.userId && typeof resource.id === "string") {
|
|
591
|
+
target.userId = resource.id;
|
|
592
|
+
}
|
|
593
|
+
const email = resource.email;
|
|
594
|
+
if (!target.userEmail && typeof email === "string") {
|
|
595
|
+
target.userEmail = email;
|
|
596
|
+
}
|
|
679
597
|
}
|
|
680
|
-
return typeof content === "string" ? content : JSON.stringify(content);
|
|
681
|
-
}
|
|
682
|
-
};
|
|
683
|
-
function createAIProvider(config) {
|
|
684
|
-
switch (config.provider) {
|
|
685
|
-
case "anthropic":
|
|
686
|
-
return new AnthropicProvider(config);
|
|
687
|
-
case "openai":
|
|
688
|
-
return new OpenAIProvider(config);
|
|
689
|
-
case "ollama":
|
|
690
|
-
return new OllamaProvider(config);
|
|
691
598
|
}
|
|
692
599
|
}
|
|
693
600
|
|
|
694
|
-
// src/
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if (locator.text) parts.push(`text: "${locator.text}"`);
|
|
699
|
-
if (locator.css) parts.push(`css: "${locator.css}"`);
|
|
700
|
-
if (locator.xpath) parts.push(`xpath: "${locator.xpath}"`);
|
|
701
|
-
if (locator.role) parts.push(`role: "${locator.role}"`);
|
|
702
|
-
if (locator.name) parts.push(`name: "${locator.name}"`);
|
|
703
|
-
if (locator.description) parts.push(`description: "${locator.description}"`);
|
|
704
|
-
return parts.join(", ");
|
|
705
|
-
}
|
|
706
|
-
function formatAction(action) {
|
|
707
|
-
switch (action.type) {
|
|
708
|
-
case "tap":
|
|
709
|
-
return `tap on element (${formatLocator(action.target)})`;
|
|
710
|
-
case "input":
|
|
711
|
-
return `input into element (${formatLocator(action.target)})`;
|
|
712
|
-
case "assert":
|
|
713
|
-
return `assert element exists (${formatLocator(action.target)})`;
|
|
714
|
-
case "wait":
|
|
715
|
-
return action.target ? `wait for element (${formatLocator(action.target)})` : `wait ${action.timeout}ms`;
|
|
716
|
-
case "scroll":
|
|
717
|
-
return action.target ? `scroll to element (${formatLocator(action.target)})` : `scroll ${action.direction || "down"}`;
|
|
718
|
-
default:
|
|
719
|
-
return action.type;
|
|
601
|
+
// src/integrations/email/inbucketClient.ts
|
|
602
|
+
var InbucketClient = class {
|
|
603
|
+
constructor(config) {
|
|
604
|
+
this.endpoint = config.endpoint.replace(/\/$/, "");
|
|
720
605
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
explanation: "AI configuration not provided. Cannot generate suggestions."
|
|
727
|
-
};
|
|
606
|
+
/**
|
|
607
|
+
* Extract mailbox name from email (e.g., "test@example.com" → "test")
|
|
608
|
+
*/
|
|
609
|
+
getMailboxName(email) {
|
|
610
|
+
return email.split("@")[0];
|
|
728
611
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
"css": "string (optional)",
|
|
741
|
-
"role": "string (optional)",
|
|
742
|
-
"name": "string (optional)"
|
|
743
|
-
},
|
|
744
|
-
"explanation": "string explaining why this selector is better"
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
Prefer selectors in this order:
|
|
748
|
-
1. testId (most reliable)
|
|
749
|
-
2. text (good for user-facing elements)
|
|
750
|
-
3. role with name (semantic and accessible)
|
|
751
|
-
4. css (last resort, but can be precise)
|
|
752
|
-
|
|
753
|
-
Do not suggest xpath unless absolutely necessary.`;
|
|
754
|
-
const prompt = `Action failed: ${formatAction(action)}
|
|
755
|
-
|
|
756
|
-
Error message:
|
|
757
|
-
${error}
|
|
758
|
-
|
|
759
|
-
Page content (truncated to 10000 chars):
|
|
760
|
-
${pageContent.slice(0, 1e4)}
|
|
761
|
-
|
|
762
|
-
${screenshot ? "[Screenshot attached but not analyzed in this implementation]" : ""}
|
|
763
|
-
|
|
764
|
-
Please analyze the error and suggest a better selector that would work reliably. Focus on:
|
|
765
|
-
- What went wrong with the current selector
|
|
766
|
-
- What selector would be more reliable
|
|
767
|
-
- Why the suggested selector is better
|
|
768
|
-
|
|
769
|
-
Return ONLY valid JSON, no additional text.`;
|
|
770
|
-
const response = await provider.generateCompletion(prompt, systemPrompt);
|
|
771
|
-
let jsonStr = response.trim();
|
|
772
|
-
if (jsonStr.startsWith("```json")) {
|
|
773
|
-
jsonStr = jsonStr.replace(/^```json\s*/, "").replace(/\s*```$/, "");
|
|
774
|
-
} else if (jsonStr.startsWith("```")) {
|
|
775
|
-
jsonStr = jsonStr.replace(/^```\s*/, "").replace(/\s*```$/, "");
|
|
612
|
+
/**
|
|
613
|
+
* List all messages in a mailbox
|
|
614
|
+
*/
|
|
615
|
+
async listMessages(email) {
|
|
616
|
+
const mailbox = this.getMailboxName(email);
|
|
617
|
+
const url = `${this.endpoint}/api/v1/mailbox/${mailbox}`;
|
|
618
|
+
const response = await fetch(url);
|
|
619
|
+
if (!response.ok) {
|
|
620
|
+
throw new Error(
|
|
621
|
+
`Failed to list messages for ${email}: ${response.status} ${response.statusText}`
|
|
622
|
+
);
|
|
776
623
|
}
|
|
777
|
-
const
|
|
778
|
-
return
|
|
779
|
-
} catch (err) {
|
|
780
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
781
|
-
return {
|
|
782
|
-
hasSuggestion: false,
|
|
783
|
-
explanation: `Failed to generate AI suggestion: ${message}`
|
|
784
|
-
};
|
|
624
|
+
const messages = await response.json();
|
|
625
|
+
return messages;
|
|
785
626
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
627
|
+
/**
|
|
628
|
+
* Get a specific message
|
|
629
|
+
*/
|
|
630
|
+
async getMessage(email, id) {
|
|
631
|
+
const mailbox = this.getMailboxName(email);
|
|
632
|
+
const url = `${this.endpoint}/api/v1/mailbox/${mailbox}/${id}`;
|
|
633
|
+
const response = await fetch(url);
|
|
634
|
+
if (!response.ok) {
|
|
635
|
+
throw new Error(
|
|
636
|
+
`Failed to get message ${id} for ${email}: ${response.status} ${response.statusText}`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
const message = await response.json();
|
|
640
|
+
return message;
|
|
792
641
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
this.
|
|
803
|
-
|
|
804
|
-
if (
|
|
805
|
-
|
|
806
|
-
resolve();
|
|
807
|
-
} else {
|
|
808
|
-
reject(new Error("Failed to get server port"));
|
|
642
|
+
/**
|
|
643
|
+
* Wait for an email to arrive (polling with timeout)
|
|
644
|
+
*/
|
|
645
|
+
async waitForEmail(email, options) {
|
|
646
|
+
const timeout = options?.timeout ?? 3e4;
|
|
647
|
+
const pollInterval = options?.pollInterval ?? 1e3;
|
|
648
|
+
const subjectContains = options?.subjectContains;
|
|
649
|
+
const startTime = Date.now();
|
|
650
|
+
while (Date.now() - startTime < timeout) {
|
|
651
|
+
const messages = await this.listMessages(email);
|
|
652
|
+
const matchingMessage = messages.find((msg) => {
|
|
653
|
+
if (subjectContains) {
|
|
654
|
+
return msg.subject.includes(subjectContains);
|
|
809
655
|
}
|
|
656
|
+
return true;
|
|
810
657
|
});
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
816
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
817
|
-
if (req.method === "OPTIONS") {
|
|
818
|
-
res.writeHead(204);
|
|
819
|
-
res.end();
|
|
820
|
-
return;
|
|
658
|
+
if (matchingMessage) {
|
|
659
|
+
return await this.getMessage(email, matchingMessage.id);
|
|
660
|
+
}
|
|
661
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
821
662
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
663
|
+
throw new Error(
|
|
664
|
+
`Timeout waiting for email to ${email}${subjectContains ? ` with subject containing "${subjectContains}"` : ""}`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Extract verification code from email body
|
|
669
|
+
*/
|
|
670
|
+
extractCode(email, pattern) {
|
|
671
|
+
const regex = pattern ?? /\b(\d{6})\b/;
|
|
672
|
+
const text = email.body.text || email.body.html;
|
|
673
|
+
const match = text.match(regex);
|
|
674
|
+
return match ? match[1] : null;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Extract link from email body
|
|
678
|
+
*/
|
|
679
|
+
extractLink(email, pattern) {
|
|
680
|
+
const regex = pattern ?? /https?:\/\/[^\s"'<>]+/;
|
|
681
|
+
const text = email.body.text || email.body.html;
|
|
682
|
+
const match = text.match(regex);
|
|
683
|
+
return match ? match[0] : null;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Delete a specific message
|
|
687
|
+
*/
|
|
688
|
+
async deleteMessage(email, id) {
|
|
689
|
+
const mailbox = this.getMailboxName(email);
|
|
690
|
+
const url = `${this.endpoint}/api/v1/mailbox/${mailbox}/${id}`;
|
|
691
|
+
const response = await fetch(url, {
|
|
692
|
+
method: "DELETE"
|
|
693
|
+
});
|
|
694
|
+
if (!response.ok) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
`Failed to delete message ${id} for ${email}: ${response.status} ${response.statusText}`
|
|
697
|
+
);
|
|
830
698
|
}
|
|
831
699
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
700
|
+
/**
|
|
701
|
+
* Clear all messages in a mailbox
|
|
702
|
+
*/
|
|
703
|
+
async clearMailbox(email) {
|
|
704
|
+
const mailbox = this.getMailboxName(email);
|
|
705
|
+
const url = `${this.endpoint}/api/v1/mailbox/${mailbox}`;
|
|
706
|
+
const response = await fetch(url, {
|
|
707
|
+
method: "DELETE"
|
|
836
708
|
});
|
|
837
|
-
|
|
709
|
+
if (!response.ok) {
|
|
710
|
+
throw new Error(
|
|
711
|
+
`Failed to clear mailbox for ${email}: ${response.status} ${response.statusText}`
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
var AppwriteTestClient = class {
|
|
717
|
+
constructor(config) {
|
|
718
|
+
this.config = config;
|
|
719
|
+
this.client = new Client().setEndpoint(config.endpoint).setProject(config.projectId).setKey(config.apiKey);
|
|
720
|
+
this.users = new Users(this.client);
|
|
721
|
+
this.tablesDB = new TablesDB(this.client);
|
|
722
|
+
this.storage = new Storage(this.client);
|
|
723
|
+
this.teams = new Teams(this.client);
|
|
724
|
+
}
|
|
725
|
+
async cleanup(context, sessionId, cwd) {
|
|
726
|
+
const deleted = [];
|
|
727
|
+
const failed = [];
|
|
728
|
+
const sortedResources = [...context.resources].reverse();
|
|
729
|
+
for (const resource of sortedResources) {
|
|
730
|
+
if (resource.deleted) {
|
|
731
|
+
deleted.push(`${resource.type}:${resource.id} (already deleted)`);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
838
734
|
try {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
735
|
+
switch (resource.type) {
|
|
736
|
+
case "row":
|
|
737
|
+
if (resource.databaseId && resource.tableId) {
|
|
738
|
+
await this.tablesDB.deleteRow({
|
|
739
|
+
databaseId: resource.databaseId,
|
|
740
|
+
tableId: resource.tableId,
|
|
741
|
+
rowId: resource.id
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
break;
|
|
745
|
+
case "file":
|
|
746
|
+
if (resource.bucketId) {
|
|
747
|
+
await this.storage.deleteFile(resource.bucketId, resource.id);
|
|
748
|
+
}
|
|
749
|
+
break;
|
|
750
|
+
case "membership":
|
|
751
|
+
if (resource.teamId) {
|
|
752
|
+
await this.teams.deleteMembership(resource.teamId, resource.id);
|
|
753
|
+
}
|
|
754
|
+
break;
|
|
755
|
+
case "team":
|
|
756
|
+
await this.teams.delete(resource.id);
|
|
757
|
+
break;
|
|
758
|
+
case "message":
|
|
759
|
+
deleted.push(`${resource.type}:${resource.id} (skipped - messages cannot be deleted)`);
|
|
760
|
+
continue;
|
|
761
|
+
case "user":
|
|
762
|
+
break;
|
|
844
763
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
id: trackRequest.id,
|
|
850
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
851
|
-
};
|
|
852
|
-
const sessionResources = this.resources.get(sessionId) || [];
|
|
853
|
-
sessionResources.push(resource);
|
|
854
|
-
this.resources.set(sessionId, sessionResources);
|
|
855
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
856
|
-
res.end(JSON.stringify({ success: true }));
|
|
857
|
-
} catch {
|
|
858
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
859
|
-
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
764
|
+
deleted.push(`${resource.type}:${resource.id}`);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
failed.push(`${resource.type}:${resource.id}`);
|
|
767
|
+
console.warn(`Failed to delete ${resource.type} ${resource.id}:`, error);
|
|
860
768
|
}
|
|
861
|
-
});
|
|
862
|
-
req.on("error", () => {
|
|
863
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
864
|
-
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
handleGetResources(url, res) {
|
|
868
|
-
const sessionId = url.pathname.split("/").pop();
|
|
869
|
-
if (!sessionId) {
|
|
870
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
871
|
-
res.end(JSON.stringify({ error: "Missing sessionId" }));
|
|
872
|
-
return;
|
|
873
769
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
770
|
+
if (context.userId) {
|
|
771
|
+
try {
|
|
772
|
+
await this.users.delete(context.userId);
|
|
773
|
+
deleted.push(`user:${context.userId}`);
|
|
774
|
+
} catch (error) {
|
|
775
|
+
failed.push(`user:${context.userId}`);
|
|
776
|
+
console.warn(`Failed to delete user ${context.userId}:`, error);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (failed.length > 0 && sessionId) {
|
|
780
|
+
const failedResources = context.resources.filter(
|
|
781
|
+
(r) => failed.some((f) => f.includes(r.id))
|
|
782
|
+
);
|
|
783
|
+
await saveFailedCleanup({
|
|
784
|
+
sessionId,
|
|
785
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
786
|
+
resources: failedResources.map((r) => ({
|
|
787
|
+
type: r.type,
|
|
788
|
+
id: r.id,
|
|
789
|
+
databaseId: r.databaseId,
|
|
790
|
+
tableId: r.tableId,
|
|
791
|
+
bucketId: r.bucketId,
|
|
792
|
+
teamId: r.teamId
|
|
793
|
+
})),
|
|
794
|
+
providerConfig: {
|
|
795
|
+
provider: "appwrite",
|
|
796
|
+
endpoint: this.config.endpoint,
|
|
797
|
+
projectId: this.config.projectId
|
|
798
|
+
},
|
|
799
|
+
errors: failed
|
|
800
|
+
}, cwd);
|
|
801
|
+
}
|
|
802
|
+
return { success: failed.length === 0, deleted, failed };
|
|
899
803
|
}
|
|
900
804
|
};
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
805
|
+
function createTestContext() {
|
|
806
|
+
return {
|
|
807
|
+
resources: [],
|
|
808
|
+
variables: /* @__PURE__ */ new Map()
|
|
809
|
+
};
|
|
905
810
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
var
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
811
|
+
|
|
812
|
+
// src/integrations/appwrite/types.ts
|
|
813
|
+
var APPWRITE_PATTERNS = {
|
|
814
|
+
userCreate: /\/v1\/account$/,
|
|
815
|
+
rowCreate: /\/v1\/tablesdb\/([\w-]+)\/tables\/([\w-]+)\/rows$/,
|
|
816
|
+
fileCreate: /\/v1\/storage\/buckets\/([\w-]+)\/files$/,
|
|
817
|
+
teamCreate: /\/v1\/teams$/,
|
|
818
|
+
membershipCreate: /\/v1\/teams\/([\w-]+)\/memberships$/,
|
|
819
|
+
messageCreate: /\/v1\/messaging\/messages$/
|
|
820
|
+
};
|
|
821
|
+
var APPWRITE_UPDATE_PATTERNS = {
|
|
822
|
+
rowUpdate: /\/v1\/tablesdb\/([\w-]+)\/tables\/([\w-]+)\/rows\/([\w-]+)$/,
|
|
823
|
+
fileUpdate: /\/v1\/storage\/buckets\/([\w-]+)\/files\/([\w-]+)$/,
|
|
824
|
+
teamUpdate: /\/v1\/teams\/([\w-]+)$/
|
|
825
|
+
};
|
|
826
|
+
var APPWRITE_DELETE_PATTERNS = {
|
|
827
|
+
rowDelete: /\/v1\/tablesdb\/([\w-]+)\/tables\/([\w-]+)\/rows\/([\w-]+)$/,
|
|
828
|
+
fileDelete: /\/v1\/storage\/buckets\/([\w-]+)\/files\/([\w-]+)$/,
|
|
829
|
+
teamDelete: /\/v1\/teams\/([\w-]+)$/,
|
|
830
|
+
membershipDelete: /\/v1\/teams\/([\w-]+)\/memberships\/([\w-]+)$/
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
// src/executors/web/browserOptions.ts
|
|
834
|
+
var VIEWPORT_SIZES = {
|
|
835
|
+
xs: { width: 320, height: 568 },
|
|
836
|
+
// Mobile portrait
|
|
837
|
+
sm: { width: 640, height: 800 },
|
|
838
|
+
// Small tablet
|
|
839
|
+
md: { width: 768, height: 1024 },
|
|
840
|
+
// Tablet
|
|
841
|
+
lg: { width: 1024, height: 768 },
|
|
842
|
+
// Desktop
|
|
843
|
+
xl: { width: 1280, height: 720 }
|
|
844
|
+
// Large desktop
|
|
845
|
+
};
|
|
846
|
+
function parseViewportSize(size) {
|
|
847
|
+
if (size in VIEWPORT_SIZES) {
|
|
848
|
+
return VIEWPORT_SIZES[size];
|
|
849
|
+
}
|
|
850
|
+
const match = size.match(/^(\d+)x(\d+)$/);
|
|
851
|
+
if (match) {
|
|
852
|
+
const width = parseInt(match[1], 10);
|
|
853
|
+
const height = parseInt(match[2], 10);
|
|
854
|
+
if (width > 0 && height > 0) {
|
|
855
|
+
return { width, height };
|
|
919
856
|
}
|
|
920
|
-
|
|
921
|
-
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
function getBrowserLaunchOptions(options) {
|
|
861
|
+
const { headless, browser } = options;
|
|
862
|
+
if (browser === "chromium" || !browser) {
|
|
922
863
|
return {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
864
|
+
headless,
|
|
865
|
+
args: [
|
|
866
|
+
// Use Chrome's new headless mode which behaves more like regular browser
|
|
867
|
+
...headless ? ["--headless=new"] : [],
|
|
868
|
+
// For Docker/CI environments - disables sandboxing (required for root/container)
|
|
869
|
+
"--no-sandbox",
|
|
870
|
+
// Shared memory - critical for Docker/CI, harmless locally
|
|
871
|
+
"--disable-dev-shm-usage",
|
|
872
|
+
// GPU acceleration - not needed in headless mode
|
|
873
|
+
"--disable-gpu",
|
|
874
|
+
// Set explicit window size (fallback for viewport)
|
|
875
|
+
"--window-size=1920,1080",
|
|
876
|
+
// Reduce overhead
|
|
877
|
+
"--disable-extensions",
|
|
878
|
+
// Prevent JavaScript throttling (helps with slow page loads)
|
|
879
|
+
"--disable-background-timer-throttling",
|
|
880
|
+
"--disable-backgrounding-occluded-windows",
|
|
881
|
+
"--disable-renderer-backgrounding",
|
|
882
|
+
// Process isolation - reduces overhead for testing
|
|
883
|
+
"--disable-features=IsolateOrigins,site-per-process",
|
|
884
|
+
// Networking tweaks
|
|
885
|
+
"--disable-features=VizDisplayCompositor",
|
|
886
|
+
"--disable-blink-features=AutomationControlled"
|
|
887
|
+
]
|
|
926
888
|
};
|
|
927
889
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
var readTrackedResources = async (trackFile) => {
|
|
940
|
-
try {
|
|
941
|
-
const content = await fs__default.readFile(trackFile, "utf8");
|
|
942
|
-
if (!content.trim()) return [];
|
|
943
|
-
return content.split("\n").filter(Boolean).map((line) => {
|
|
944
|
-
try {
|
|
945
|
-
return JSON.parse(line);
|
|
946
|
-
} catch {
|
|
947
|
-
return null;
|
|
890
|
+
if (browser === "firefox") {
|
|
891
|
+
return {
|
|
892
|
+
headless,
|
|
893
|
+
firefoxUserPrefs: {
|
|
894
|
+
// Enable JIT (disabled in automation mode by default, causes 2-4x JS slowdown)
|
|
895
|
+
"javascript.options.jit": true,
|
|
896
|
+
// Disable fission for stability/speed
|
|
897
|
+
"fission.autostart": false,
|
|
898
|
+
// Disable hardware acceleration overhead in headless
|
|
899
|
+
"layers.acceleration.disabled": true,
|
|
900
|
+
"gfx.webrender.all": false
|
|
948
901
|
}
|
|
949
|
-
}
|
|
950
|
-
} catch {
|
|
951
|
-
return [];
|
|
902
|
+
};
|
|
952
903
|
}
|
|
953
|
-
};
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
providerConfig: entry.providerConfig,
|
|
967
|
-
cwd: entry.cwd,
|
|
968
|
-
config: { ...cleanupConfig, scanUntracked: false },
|
|
969
|
-
provider
|
|
904
|
+
return { headless };
|
|
905
|
+
}
|
|
906
|
+
function resolveEnvVars(value) {
|
|
907
|
+
return value.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] || "");
|
|
908
|
+
}
|
|
909
|
+
var AnthropicProvider = class {
|
|
910
|
+
constructor(config) {
|
|
911
|
+
this.config = config;
|
|
912
|
+
const apiKey = config.apiKey ? resolveEnvVars(config.apiKey) : void 0;
|
|
913
|
+
this.client = new Anthropic({
|
|
914
|
+
apiKey,
|
|
915
|
+
model: this.config.model,
|
|
916
|
+
temperature: this.config.temperature
|
|
970
917
|
});
|
|
971
|
-
}
|
|
918
|
+
}
|
|
919
|
+
async generateCompletion(prompt, systemPrompt) {
|
|
920
|
+
const messages = [];
|
|
921
|
+
if (systemPrompt) {
|
|
922
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
923
|
+
}
|
|
924
|
+
messages.push({ role: "user", content: prompt });
|
|
925
|
+
const response = await this.client.chat({ messages });
|
|
926
|
+
const content = response.message.content;
|
|
927
|
+
if (!content) {
|
|
928
|
+
throw new Error("No content in Anthropic response");
|
|
929
|
+
}
|
|
930
|
+
return typeof content === "string" ? content : JSON.stringify(content);
|
|
972
931
|
}
|
|
973
932
|
};
|
|
974
|
-
var
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
const
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
991
|
-
} catch {
|
|
992
|
-
}
|
|
933
|
+
var OpenAIProvider = class {
|
|
934
|
+
constructor(config) {
|
|
935
|
+
this.config = config;
|
|
936
|
+
const apiKey = config.apiKey ? resolveEnvVars(config.apiKey) : void 0;
|
|
937
|
+
const baseURL = config.baseUrl;
|
|
938
|
+
this.client = new OpenAI({
|
|
939
|
+
apiKey,
|
|
940
|
+
model: this.config.model,
|
|
941
|
+
temperature: this.config.temperature,
|
|
942
|
+
baseURL
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
async generateCompletion(prompt, systemPrompt) {
|
|
946
|
+
const messages = [];
|
|
947
|
+
if (systemPrompt) {
|
|
948
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
993
949
|
}
|
|
994
|
-
|
|
950
|
+
messages.push({ role: "user", content: prompt });
|
|
951
|
+
const response = await this.client.chat({ messages });
|
|
952
|
+
const content = response.message.content;
|
|
953
|
+
if (!content) {
|
|
954
|
+
throw new Error("No content in OpenAI response");
|
|
955
|
+
}
|
|
956
|
+
return typeof content === "string" ? content : JSON.stringify(content);
|
|
995
957
|
}
|
|
996
958
|
};
|
|
997
|
-
var
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
959
|
+
var OllamaProvider = class {
|
|
960
|
+
constructor(config) {
|
|
961
|
+
this.config = config;
|
|
962
|
+
this.client = new Ollama({
|
|
963
|
+
model: this.config.model,
|
|
964
|
+
options: {
|
|
965
|
+
temperature: this.config.temperature
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
async generateCompletion(prompt, systemPrompt) {
|
|
970
|
+
const messages = [];
|
|
971
|
+
if (systemPrompt) {
|
|
972
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
1009
973
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
} catch {
|
|
974
|
+
messages.push({ role: "user", content: prompt });
|
|
975
|
+
const response = await this.client.chat({ messages });
|
|
976
|
+
const content = response.message.content;
|
|
977
|
+
if (!content) {
|
|
978
|
+
throw new Error("No content in Ollama response");
|
|
1016
979
|
}
|
|
1017
|
-
|
|
1018
|
-
}
|
|
1019
|
-
if (changed) {
|
|
1020
|
-
await saveActiveTests(cwd, state, trackDir);
|
|
980
|
+
return typeof content === "string" ? content : JSON.stringify(content);
|
|
1021
981
|
}
|
|
1022
|
-
await cleanupOrphanedTrackFiles(cwd, state, trackDir);
|
|
1023
982
|
};
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
sessionId: options.sessionId,
|
|
1034
|
-
trackFile,
|
|
1035
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1036
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1037
|
-
cwd,
|
|
1038
|
-
providerConfig: options.providerConfig
|
|
1039
|
-
};
|
|
1040
|
-
await saveActiveTests(cwd, state, trackDir);
|
|
1041
|
-
const touch = async () => {
|
|
1042
|
-
const current = await loadActiveTests(cwd, trackDir);
|
|
1043
|
-
const entry = current.sessions[options.sessionId];
|
|
1044
|
-
if (!entry) return;
|
|
1045
|
-
entry.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1046
|
-
current.sessions[options.sessionId] = entry;
|
|
1047
|
-
await saveActiveTests(cwd, current, trackDir);
|
|
1048
|
-
};
|
|
1049
|
-
const interval = setInterval(() => {
|
|
1050
|
-
void touch();
|
|
1051
|
-
}, HEARTBEAT_MS);
|
|
1052
|
-
const stop = async () => {
|
|
1053
|
-
clearInterval(interval);
|
|
1054
|
-
const current = await loadActiveTests(cwd, trackDir);
|
|
1055
|
-
delete current.sessions[options.sessionId];
|
|
1056
|
-
await saveActiveTests(cwd, current, trackDir);
|
|
1057
|
-
try {
|
|
1058
|
-
await fs__default.rm(trackFile, { force: true });
|
|
1059
|
-
} catch {
|
|
1060
|
-
}
|
|
1061
|
-
};
|
|
1062
|
-
return { trackFile, stop };
|
|
983
|
+
function createAIProvider(config) {
|
|
984
|
+
switch (config.provider) {
|
|
985
|
+
case "anthropic":
|
|
986
|
+
return new AnthropicProvider(config);
|
|
987
|
+
case "openai":
|
|
988
|
+
return new OpenAIProvider(config);
|
|
989
|
+
case "ollama":
|
|
990
|
+
return new OllamaProvider(config);
|
|
991
|
+
}
|
|
1063
992
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
993
|
+
|
|
994
|
+
// src/ai/errorHelper.ts
|
|
995
|
+
function formatLocator(locator) {
|
|
996
|
+
const parts = [];
|
|
997
|
+
if (locator.testId) parts.push(`testId: "${locator.testId}"`);
|
|
998
|
+
if (locator.text) parts.push(`text: "${locator.text}"`);
|
|
999
|
+
if (locator.css) parts.push(`css: "${locator.css}"`);
|
|
1000
|
+
if (locator.xpath) parts.push(`xpath: "${locator.xpath}"`);
|
|
1001
|
+
if (locator.role) parts.push(`role: "${locator.role}"`);
|
|
1002
|
+
if (locator.name) parts.push(`name: "${locator.name}"`);
|
|
1003
|
+
if (locator.description) parts.push(`description: "${locator.description}"`);
|
|
1004
|
+
return parts.join(", ");
|
|
1005
|
+
}
|
|
1006
|
+
function formatAction(action) {
|
|
1007
|
+
switch (action.type) {
|
|
1008
|
+
case "tap":
|
|
1009
|
+
return `tap on element (${formatLocator(action.target)})`;
|
|
1010
|
+
case "input":
|
|
1011
|
+
return `input into element (${formatLocator(action.target)})`;
|
|
1012
|
+
case "assert":
|
|
1013
|
+
return `assert element exists (${formatLocator(action.target)})`;
|
|
1014
|
+
case "wait":
|
|
1015
|
+
return action.target ? `wait for element (${formatLocator(action.target)})` : `wait ${action.timeout}ms`;
|
|
1016
|
+
case "scroll":
|
|
1017
|
+
return action.target ? `scroll to element (${formatLocator(action.target)})` : `scroll ${action.direction || "down"}`;
|
|
1018
|
+
default:
|
|
1019
|
+
return action.type;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
async function getAISuggestion(error, action, pageContent, screenshot, aiConfig) {
|
|
1023
|
+
if (!aiConfig) {
|
|
1024
|
+
return {
|
|
1025
|
+
hasSuggestion: false,
|
|
1026
|
+
explanation: "AI configuration not provided. Cannot generate suggestions."
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
try {
|
|
1030
|
+
const provider = createAIProvider(aiConfig);
|
|
1031
|
+
const systemPrompt = `You are an expert at analyzing web automation errors and suggesting better element selectors.
|
|
1032
|
+
Your task is to analyze failed actions and suggest better selectors based on the page content and error message.
|
|
1033
|
+
|
|
1034
|
+
Return your response in the following JSON format:
|
|
1035
|
+
{
|
|
1036
|
+
"hasSuggestion": boolean,
|
|
1037
|
+
"suggestedSelector": {
|
|
1038
|
+
"testId": "string (optional)",
|
|
1039
|
+
"text": "string (optional)",
|
|
1040
|
+
"css": "string (optional)",
|
|
1041
|
+
"role": "string (optional)",
|
|
1042
|
+
"name": "string (optional)"
|
|
1043
|
+
},
|
|
1044
|
+
"explanation": "string explaining why this selector is better"
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
Prefer selectors in this order:
|
|
1048
|
+
1. testId (most reliable)
|
|
1049
|
+
2. text (good for user-facing elements)
|
|
1050
|
+
3. role with name (semantic and accessible)
|
|
1051
|
+
4. css (last resort, but can be precise)
|
|
1052
|
+
|
|
1053
|
+
Do not suggest xpath unless absolutely necessary.`;
|
|
1054
|
+
const prompt = `Action failed: ${formatAction(action)}
|
|
1055
|
+
|
|
1056
|
+
Error message:
|
|
1057
|
+
${error}
|
|
1058
|
+
|
|
1059
|
+
Page content (truncated to 10000 chars):
|
|
1060
|
+
${pageContent.slice(0, 1e4)}
|
|
1061
|
+
|
|
1062
|
+
${screenshot ? "[Screenshot attached but not analyzed in this implementation]" : ""}
|
|
1063
|
+
|
|
1064
|
+
Please analyze the error and suggest a better selector that would work reliably. Focus on:
|
|
1065
|
+
- What went wrong with the current selector
|
|
1066
|
+
- What selector would be more reliable
|
|
1067
|
+
- Why the suggested selector is better
|
|
1068
|
+
|
|
1069
|
+
Return ONLY valid JSON, no additional text.`;
|
|
1070
|
+
const response = await provider.generateCompletion(prompt, systemPrompt);
|
|
1071
|
+
let jsonStr = response.trim();
|
|
1072
|
+
if (jsonStr.startsWith("```json")) {
|
|
1073
|
+
jsonStr = jsonStr.replace(/^```json\s*/, "").replace(/\s*```$/, "");
|
|
1074
|
+
} else if (jsonStr.startsWith("```")) {
|
|
1075
|
+
jsonStr = jsonStr.replace(/^```\s*/, "").replace(/\s*```$/, "");
|
|
1084
1076
|
}
|
|
1077
|
+
const parsed = JSON.parse(jsonStr);
|
|
1078
|
+
return parsed;
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1081
|
+
return {
|
|
1082
|
+
hasSuggestion: false,
|
|
1083
|
+
explanation: `Failed to generate AI suggestion: ${message}`
|
|
1084
|
+
};
|
|
1085
1085
|
}
|
|
1086
1086
|
}
|
|
1087
1087
|
var SERVER_MARKER_FILE = "server.json";
|
|
@@ -2161,47 +2161,63 @@ var runWebTest = async (test, options = {}) => {
|
|
|
2161
2161
|
const headless = !(options.headed ?? false);
|
|
2162
2162
|
const screenshotDir = options.screenshotDir ?? defaultScreenshotDir;
|
|
2163
2163
|
const defaultTimeout = options.defaultTimeoutMs ?? 3e4;
|
|
2164
|
+
const trackingAlreadySetUp = options.skipTrackingSetup || process.env.INTELLITESTER_TRACKING_OWNER === "cli";
|
|
2165
|
+
let ownsTracking = false;
|
|
2166
|
+
let trackingServer = null;
|
|
2167
|
+
let fileTracking = null;
|
|
2164
2168
|
const sessionId = options.sessionId ?? crypto4.randomUUID();
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2169
|
+
if (!trackingAlreadySetUp) {
|
|
2170
|
+
ownsTracking = true;
|
|
2171
|
+
trackingServer = new TrackingServer();
|
|
2172
|
+
await trackingServer.start();
|
|
2173
|
+
process.env.INTELLITESTER_SESSION_ID = sessionId;
|
|
2174
|
+
process.env.INTELLITESTER_TRACK_URL = `http://localhost:${trackingServer.port}`;
|
|
2175
|
+
fileTracking = await initFileTracking({
|
|
2176
|
+
sessionId,
|
|
2177
|
+
trackDir: options.trackDir,
|
|
2178
|
+
cleanupConfig: test.config?.appwrite?.cleanup ? {
|
|
2179
|
+
provider: "appwrite",
|
|
2180
|
+
scanUntracked: true,
|
|
2181
|
+
appwrite: {
|
|
2182
|
+
endpoint: test.config.appwrite.endpoint,
|
|
2183
|
+
projectId: test.config.appwrite.projectId,
|
|
2184
|
+
apiKey: test.config.appwrite.apiKey,
|
|
2185
|
+
cleanupOnFailure: test.config.appwrite.cleanupOnFailure
|
|
2186
|
+
}
|
|
2187
|
+
} : void 0,
|
|
2188
|
+
providerConfig: test.config?.appwrite ? {
|
|
2189
|
+
provider: "appwrite",
|
|
2176
2190
|
endpoint: test.config.appwrite.endpoint,
|
|
2177
2191
|
projectId: test.config.appwrite.projectId,
|
|
2178
|
-
apiKey: test.config.appwrite.apiKey
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
apiKey: test.config.appwrite.apiKey
|
|
2187
|
-
} : void 0
|
|
2188
|
-
});
|
|
2189
|
-
process.env.INTELLITESTER_TRACK_FILE = fileTracking.trackFile;
|
|
2190
|
-
if (options.webServer) {
|
|
2192
|
+
apiKey: test.config.appwrite.apiKey
|
|
2193
|
+
} : void 0
|
|
2194
|
+
});
|
|
2195
|
+
process.env.INTELLITESTER_TRACK_FILE = fileTracking.trackFile;
|
|
2196
|
+
} else {
|
|
2197
|
+
console.log("Using existing tracking setup (owned by CLI)");
|
|
2198
|
+
}
|
|
2199
|
+
if (options.webServer && !options.skipWebServerStart) {
|
|
2191
2200
|
const requiresTrackingEnv = Boolean(
|
|
2192
2201
|
test.config?.appwrite?.cleanup || test.config?.appwrite?.cleanupOnFailure
|
|
2193
2202
|
);
|
|
2194
|
-
const
|
|
2195
|
-
|
|
2203
|
+
const userExplicitlySetReuse = options.webServer.reuseExistingServer !== void 0;
|
|
2204
|
+
const webServerConfig = ownsTracking && requiresTrackingEnv && !userExplicitlySetReuse ? { ...options.webServer, reuseExistingServer: false } : options.webServer;
|
|
2205
|
+
if (ownsTracking && requiresTrackingEnv && !userExplicitlySetReuse) {
|
|
2196
2206
|
console.log("[Intellitester] Appwrite cleanup enabled; restarting server to inject tracking env.");
|
|
2197
2207
|
}
|
|
2198
2208
|
await webServerManager.start(webServerConfig);
|
|
2209
|
+
} else if (options.skipWebServerStart) {
|
|
2210
|
+
console.log("Using existing web server (owned by CLI)");
|
|
2199
2211
|
}
|
|
2200
2212
|
const cleanup = () => {
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2213
|
+
if (ownsTracking) {
|
|
2214
|
+
trackingServer?.stop();
|
|
2215
|
+
webServerManager.kill();
|
|
2216
|
+
if (fileTracking) {
|
|
2217
|
+
void fileTracking.stop();
|
|
2218
|
+
}
|
|
2219
|
+
delete process.env.INTELLITESTER_TRACK_FILE;
|
|
2220
|
+
}
|
|
2205
2221
|
process.exit(1);
|
|
2206
2222
|
};
|
|
2207
2223
|
process.on("SIGINT", cleanup);
|
|
@@ -2451,7 +2467,7 @@ var runWebTest = async (test, options = {}) => {
|
|
|
2451
2467
|
if (debugMode) {
|
|
2452
2468
|
console.log(`[DEBUG] Executing step ${index + 1}: ${action.type}`);
|
|
2453
2469
|
}
|
|
2454
|
-
const serverResources = trackingServer
|
|
2470
|
+
const serverResources = trackingServer?.getResources(sessionId) ?? [];
|
|
2455
2471
|
for (const resource of serverResources) {
|
|
2456
2472
|
if (resource.type === "user" && !executionContext.appwriteContext.userId) {
|
|
2457
2473
|
executionContext.appwriteContext.userId = resource.id;
|
|
@@ -2532,7 +2548,7 @@ var runWebTest = async (test, options = {}) => {
|
|
|
2532
2548
|
try {
|
|
2533
2549
|
process.off("SIGINT", cleanup);
|
|
2534
2550
|
process.off("SIGTERM", cleanup);
|
|
2535
|
-
if (lastExecutionContext) {
|
|
2551
|
+
if (lastExecutionContext && ownsTracking && fileTracking) {
|
|
2536
2552
|
await mergeFileTrackedResources(
|
|
2537
2553
|
fileTracking.trackFile,
|
|
2538
2554
|
lastExecutionContext.appwriteContext
|
|
@@ -2552,11 +2568,15 @@ var runWebTest = async (test, options = {}) => {
|
|
|
2552
2568
|
console.log("Cleanup result:", cleanupResult);
|
|
2553
2569
|
}
|
|
2554
2570
|
}
|
|
2555
|
-
|
|
2556
|
-
|
|
2571
|
+
if (ownsTracking) {
|
|
2572
|
+
if (fileTracking) {
|
|
2573
|
+
await fileTracking.stop();
|
|
2574
|
+
}
|
|
2575
|
+
delete process.env.INTELLITESTER_TRACK_FILE;
|
|
2576
|
+
trackingServer?.stop();
|
|
2577
|
+
await webServerManager.stop();
|
|
2578
|
+
}
|
|
2557
2579
|
await browser.close();
|
|
2558
|
-
trackingServer.stop();
|
|
2559
|
-
await webServerManager.stop();
|
|
2560
2580
|
} catch (cleanupError) {
|
|
2561
2581
|
console.error("Error during cleanup:", cleanupError);
|
|
2562
2582
|
}
|
|
@@ -3510,56 +3530,71 @@ async function runWorkflow(workflow, workflowFilePath, options = {}) {
|
|
|
3510
3530
|
const sessionId = options.sessionId ?? crypto4.randomUUID();
|
|
3511
3531
|
const testStartTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
3512
3532
|
const cleanupConfig = inferCleanupConfig(workflow.config);
|
|
3533
|
+
const trackingAlreadySetUp = options.skipTrackingSetup || process.env.INTELLITESTER_TRACKING_OWNER === "cli";
|
|
3534
|
+
let ownsTracking = false;
|
|
3513
3535
|
let trackingServer = null;
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3536
|
+
let fileTracking = null;
|
|
3537
|
+
if (!trackingAlreadySetUp) {
|
|
3538
|
+
ownsTracking = true;
|
|
3539
|
+
try {
|
|
3540
|
+
trackingServer = await startTrackingServer({ port: 0 });
|
|
3541
|
+
console.log(`Tracking server started on port ${trackingServer.port}`);
|
|
3542
|
+
} catch (error) {
|
|
3543
|
+
console.warn("Failed to start tracking server:", error);
|
|
3544
|
+
}
|
|
3545
|
+
if (trackingServer) {
|
|
3546
|
+
process.env.INTELLITESTER_SESSION_ID = sessionId;
|
|
3547
|
+
process.env.INTELLITESTER_TRACK_URL = `http://localhost:${trackingServer.port}`;
|
|
3548
|
+
}
|
|
3549
|
+
fileTracking = await initFileTracking({
|
|
3550
|
+
sessionId,
|
|
3551
|
+
cleanupConfig,
|
|
3552
|
+
trackDir: options.trackDir,
|
|
3553
|
+
providerConfig: workflow.config?.appwrite ? {
|
|
3554
|
+
provider: "appwrite",
|
|
3555
|
+
endpoint: workflow.config.appwrite.endpoint,
|
|
3556
|
+
projectId: workflow.config.appwrite.projectId,
|
|
3557
|
+
apiKey: workflow.config.appwrite.apiKey
|
|
3558
|
+
} : void 0
|
|
3559
|
+
});
|
|
3560
|
+
process.env.INTELLITESTER_TRACK_FILE = fileTracking.trackFile;
|
|
3561
|
+
} else {
|
|
3562
|
+
console.log("Using existing tracking setup (owned by CLI)");
|
|
3523
3563
|
}
|
|
3524
|
-
const fileTracking = await initFileTracking({
|
|
3525
|
-
sessionId,
|
|
3526
|
-
cleanupConfig,
|
|
3527
|
-
trackDir: options.trackDir,
|
|
3528
|
-
providerConfig: workflow.config?.appwrite ? {
|
|
3529
|
-
provider: "appwrite",
|
|
3530
|
-
endpoint: workflow.config.appwrite.endpoint,
|
|
3531
|
-
projectId: workflow.config.appwrite.projectId,
|
|
3532
|
-
apiKey: workflow.config.appwrite.apiKey
|
|
3533
|
-
} : void 0
|
|
3534
|
-
});
|
|
3535
|
-
process.env.INTELLITESTER_TRACK_FILE = fileTracking.trackFile;
|
|
3536
3564
|
const webServerConfig = workflow.config?.webServer ?? options.webServer;
|
|
3537
|
-
|
|
3565
|
+
const skipWebServer = options.skipWebServerStart;
|
|
3566
|
+
if (webServerConfig && !skipWebServer) {
|
|
3538
3567
|
try {
|
|
3539
3568
|
const serverCwd = workflow.config?.webServer ? workflowDir : process.cwd();
|
|
3540
3569
|
const requiresTrackingEnv = Boolean(
|
|
3541
3570
|
workflow.config?.appwrite?.cleanup || workflow.config?.appwrite?.cleanupOnFailure
|
|
3542
3571
|
);
|
|
3543
|
-
const
|
|
3544
|
-
|
|
3572
|
+
const userExplicitlySetReuse = webServerConfig.reuseExistingServer !== void 0;
|
|
3573
|
+
let effectiveConfig = webServerConfig;
|
|
3574
|
+
if (requiresTrackingEnv && !userExplicitlySetReuse && ownsTracking) {
|
|
3575
|
+
effectiveConfig = { ...webServerConfig, reuseExistingServer: false };
|
|
3545
3576
|
console.log("[Intellitester] Appwrite cleanup enabled; restarting server to inject tracking env.");
|
|
3546
3577
|
}
|
|
3547
3578
|
await webServerManager.start({
|
|
3548
|
-
...
|
|
3549
|
-
workdir: path4__default.resolve(serverCwd,
|
|
3579
|
+
...effectiveConfig,
|
|
3580
|
+
workdir: path4__default.resolve(serverCwd, effectiveConfig.workdir ?? effectiveConfig.cwd ?? ".")
|
|
3550
3581
|
});
|
|
3551
3582
|
} catch (error) {
|
|
3552
3583
|
console.error("Failed to start web server:", error);
|
|
3553
3584
|
if (trackingServer) await trackingServer.stop();
|
|
3554
3585
|
throw error;
|
|
3555
3586
|
}
|
|
3587
|
+
} else if (skipWebServer) {
|
|
3588
|
+
console.log("Using existing web server (started by CLI)");
|
|
3556
3589
|
}
|
|
3557
3590
|
const signalCleanup = async () => {
|
|
3558
3591
|
console.log("\n\nInterrupted - cleaning up...");
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3592
|
+
if (ownsTracking) {
|
|
3593
|
+
if (!skipWebServer) webServerManager.kill();
|
|
3594
|
+
if (trackingServer) await trackingServer.stop();
|
|
3595
|
+
if (fileTracking) await fileTracking.stop();
|
|
3596
|
+
delete process.env.INTELLITESTER_TRACK_FILE;
|
|
3597
|
+
}
|
|
3563
3598
|
process.exit(1);
|
|
3564
3599
|
};
|
|
3565
3600
|
process.on("SIGINT", signalCleanup);
|
|
@@ -3651,7 +3686,11 @@ Collected ${serverResources.length} server-tracked resources`);
|
|
|
3651
3686
|
executionContext.appwriteContext.resources.push(...serverResources);
|
|
3652
3687
|
}
|
|
3653
3688
|
}
|
|
3654
|
-
|
|
3689
|
+
if (fileTracking) {
|
|
3690
|
+
await mergeFileTrackedResources(fileTracking.trackFile, executionContext.appwriteContext);
|
|
3691
|
+
} else if (process.env.INTELLITESTER_TRACK_FILE) {
|
|
3692
|
+
await mergeFileTrackedResources(process.env.INTELLITESTER_TRACK_FILE, executionContext.appwriteContext);
|
|
3693
|
+
}
|
|
3655
3694
|
let cleanupResult;
|
|
3656
3695
|
if (cleanupConfig) {
|
|
3657
3696
|
const appwriteConfig = cleanupConfig.appwrite;
|
|
@@ -3741,17 +3780,21 @@ Collected ${serverResources.length} server-tracked resources`);
|
|
|
3741
3780
|
process.off("SIGTERM", signalCleanup);
|
|
3742
3781
|
await browserContext.close();
|
|
3743
3782
|
await browser.close();
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3783
|
+
if (ownsTracking) {
|
|
3784
|
+
if (!skipWebServer) await webServerManager.stop();
|
|
3785
|
+
if (trackingServer) {
|
|
3786
|
+
await trackingServer.stop();
|
|
3787
|
+
}
|
|
3788
|
+
if (fileTracking) {
|
|
3789
|
+
await fileTracking.stop();
|
|
3790
|
+
}
|
|
3791
|
+
delete process.env.INTELLITESTER_SESSION_ID;
|
|
3792
|
+
delete process.env.INTELLITESTER_TRACK_URL;
|
|
3793
|
+
delete process.env.INTELLITESTER_TRACK_FILE;
|
|
3747
3794
|
}
|
|
3748
|
-
await fileTracking.stop();
|
|
3749
|
-
delete process.env.INTELLITESTER_SESSION_ID;
|
|
3750
|
-
delete process.env.INTELLITESTER_TRACK_URL;
|
|
3751
|
-
delete process.env.INTELLITESTER_TRACK_FILE;
|
|
3752
3795
|
}
|
|
3753
3796
|
}
|
|
3754
3797
|
|
|
3755
3798
|
export { createAIProvider, createTestContext, generateFillerText, generateRandomEmail, generateRandomPhone, generateRandomPhoto, generateRandomUsername, getBrowserLaunchOptions, initFileTracking, interpolateVariables, mergeFileTrackedResources, parseViewportSize, runWebTest, runWorkflow, runWorkflowWithContext, setupAppwriteTracking, startTrackingServer, webServerManager };
|
|
3756
|
-
//# sourceMappingURL=chunk-
|
|
3757
|
-
//# sourceMappingURL=chunk-
|
|
3799
|
+
//# sourceMappingURL=chunk-PGOHFXEG.js.map
|
|
3800
|
+
//# sourceMappingURL=chunk-PGOHFXEG.js.map
|