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.
@@ -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
- // src/integrations/email/inbucketClient.ts
302
- var InbucketClient = class {
303
- constructor(config) {
304
- this.endpoint = config.endpoint.replace(/\/$/, "");
300
+ var TrackingServer = class {
301
+ constructor() {
302
+ this.server = null;
303
+ this.resources = /* @__PURE__ */ new Map();
304
+ this.port = 0;
305
305
  }
306
- /**
307
- * Extract mailbox name from email (e.g., "test@example.com" "test")
308
- */
309
- getMailboxName(email) {
310
- return email.split("@")[0];
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
- * List all messages in a mailbox
314
- */
315
- async listMessages(email) {
316
- const mailbox = this.getMailboxName(email);
317
- const url = `${this.endpoint}/api/v1/mailbox/${mailbox}`;
318
- const response = await fetch(url);
319
- if (!response.ok) {
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 messages = await response.json();
325
- return messages;
326
- }
327
- /**
328
- * Get a specific message
329
- */
330
- async getMessage(email, id) {
331
- const mailbox = this.getMailboxName(email);
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
- * Wait for an email to arrive (polling with timeout)
344
- */
345
- async waitForEmail(email, options) {
346
- const timeout = options?.timeout ?? 3e4;
347
- const pollInterval = options?.pollInterval ?? 1e3;
348
- const subjectContains = options?.subjectContains;
349
- const startTime = Date.now();
350
- while (Date.now() - startTime < timeout) {
351
- const messages = await this.listMessages(email);
352
- const matchingMessage = messages.find((msg) => {
353
- if (subjectContains) {
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
- return true;
357
- });
358
- if (matchingMessage) {
359
- return await this.getMessage(email, matchingMessage.id);
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
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
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
- throw new Error(
364
- `Timeout waiting for email to ${email}${subjectContains ? ` with subject containing "${subjectContains}"` : ""}`
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
- * Extract verification code from email body
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
- * Extract link from email body
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
- * Delete a specific message
387
- */
388
- async deleteMessage(email, id) {
389
- const mailbox = this.getMailboxName(email);
390
- const url = `${this.endpoint}/api/v1/mailbox/${mailbox}/${id}`;
391
- const response = await fetch(url, {
392
- method: "DELETE"
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
- * Clear all messages in a mailbox
402
- */
403
- async clearMailbox(email) {
404
- const mailbox = this.getMailboxName(email);
405
- const url = `${this.endpoint}/api/v1/mailbox/${mailbox}`;
406
- const response = await fetch(url, {
407
- method: "DELETE"
408
- });
409
- if (!response.ok) {
410
- throw new Error(
411
- `Failed to clear mailbox for ${email}: ${response.status} ${response.statusText}`
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 AppwriteTestClient = class {
417
- constructor(config) {
418
- this.config = config;
419
- this.client = new Client().setEndpoint(config.endpoint).setProject(config.projectId).setKey(config.apiKey);
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
- // src/executors/web/browserOptions.ts
534
- var VIEWPORT_SIZES = {
535
- xs: { width: 320, height: 568 },
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
- function parseViewportSize(size) {
547
- if (size in VIEWPORT_SIZES) {
548
- return VIEWPORT_SIZES[size];
549
- }
550
- const match = size.match(/^(\d+)x(\d+)$/);
551
- if (match) {
552
- const width = parseInt(match[1], 10);
553
- const height = parseInt(match[2], 10);
554
- if (width > 0 && height > 0) {
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
- return { headless };
605
- }
606
- function resolveEnvVars(value) {
607
- return value.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] || "");
608
- }
609
- var AnthropicProvider = class {
610
- constructor(config) {
611
- this.config = config;
612
- const apiKey = config.apiKey ? resolveEnvVars(config.apiKey) : void 0;
613
- this.client = new Anthropic({
614
- apiKey,
615
- model: this.config.model,
616
- temperature: this.config.temperature
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
- async generateCompletion(prompt, systemPrompt) {
620
- const messages = [];
621
- if (systemPrompt) {
622
- messages.push({ role: "system", content: systemPrompt });
623
- }
624
- messages.push({ role: "user", content: prompt });
625
- const response = await this.client.chat({ messages });
626
- const content = response.message.content;
627
- if (!content) {
628
- throw new Error("No content in Anthropic response");
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
- return typeof content === "string" ? content : JSON.stringify(content);
507
+ } catch {
631
508
  }
632
509
  };
633
- var OpenAIProvider = class {
634
- constructor(config) {
635
- this.config = config;
636
- const apiKey = config.apiKey ? resolveEnvVars(config.apiKey) : void 0;
637
- const baseURL = config.baseUrl;
638
- this.client = new OpenAI({
639
- apiKey,
640
- model: this.config.model,
641
- temperature: this.config.temperature,
642
- baseURL
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
- messages.push({ role: "user", content: prompt });
651
- const response = await this.client.chat({ messages });
652
- const content = response.message.content;
653
- if (!content) {
654
- throw new Error("No content in OpenAI response");
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
- return typeof content === "string" ? content : JSON.stringify(content);
530
+ delete state.sessions[sessionId];
657
531
  }
658
- };
659
- var OllamaProvider = class {
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
- async generateCompletion(prompt, systemPrompt) {
670
- const messages = [];
671
- if (systemPrompt) {
672
- messages.push({ role: "system", content: systemPrompt });
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
- messages.push({ role: "user", content: prompt });
675
- const response = await this.client.chat({ messages });
676
- const content = response.message.content;
677
- if (!content) {
678
- throw new Error("No content in Ollama response");
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/ai/errorHelper.ts
695
- function formatLocator(locator) {
696
- const parts = [];
697
- if (locator.testId) parts.push(`testId: "${locator.testId}"`);
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
- async function getAISuggestion(error, action, pageContent, screenshot, aiConfig) {
723
- if (!aiConfig) {
724
- return {
725
- hasSuggestion: false,
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
- try {
730
- const provider = createAIProvider(aiConfig);
731
- const systemPrompt = `You are an expert at analyzing web automation errors and suggesting better element selectors.
732
- Your task is to analyze failed actions and suggest better selectors based on the page content and error message.
733
-
734
- Return your response in the following JSON format:
735
- {
736
- "hasSuggestion": boolean,
737
- "suggestedSelector": {
738
- "testId": "string (optional)",
739
- "text": "string (optional)",
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 parsed = JSON.parse(jsonStr);
778
- return parsed;
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
- var TrackingServer = class {
788
- constructor() {
789
- this.server = null;
790
- this.resources = /* @__PURE__ */ new Map();
791
- this.port = 0;
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
- async start(options = {}) {
794
- return new Promise((resolve, reject) => {
795
- this.server = createServer((req, res) => {
796
- this.handleRequest(req, res);
797
- });
798
- this.server.on("error", (error) => {
799
- reject(error);
800
- });
801
- const port = options.port ?? 0;
802
- this.server.listen(port, () => {
803
- const address = this.server?.address();
804
- if (address && typeof address === "object") {
805
- this.port = address.port;
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
- handleRequest(req, res) {
814
- res.setHeader("Access-Control-Allow-Origin", "*");
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
- const url = new URL(req.url || "/", `http://${req.headers.host}`);
823
- if (req.method === "POST" && url.pathname === "/track") {
824
- this.handleTrackRequest(req, res);
825
- } else if (req.method === "GET" && url.pathname.startsWith("/resources/")) {
826
- this.handleGetResources(url, res);
827
- } else {
828
- res.writeHead(404, { "Content-Type": "application/json" });
829
- res.end(JSON.stringify({ error: "Not found" }));
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
- handleTrackRequest(req, res) {
833
- let body = "";
834
- req.on("data", (chunk) => {
835
- body += chunk.toString();
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
- req.on("end", () => {
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
- const trackRequest = JSON.parse(body);
840
- if (!trackRequest.sessionId || !trackRequest.type || !trackRequest.id) {
841
- res.writeHead(400, { "Content-Type": "application/json" });
842
- res.end(JSON.stringify({ error: "Missing required fields (sessionId, type, id)" }));
843
- return;
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
- const { sessionId, ...resourceData } = trackRequest;
846
- const resource = {
847
- ...resourceData,
848
- type: trackRequest.type,
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
- const resources = this.resources.get(sessionId) || [];
875
- res.writeHead(200, { "Content-Type": "application/json" });
876
- res.end(JSON.stringify({ resources }));
877
- }
878
- getResources(sessionId) {
879
- return this.resources.get(sessionId) ?? [];
880
- }
881
- clearSession(sessionId) {
882
- this.resources.delete(sessionId);
883
- }
884
- async stop() {
885
- return new Promise((resolve, reject) => {
886
- if (!this.server) {
887
- resolve();
888
- return;
889
- }
890
- this.server.close((error) => {
891
- if (error) {
892
- reject(error);
893
- } else {
894
- this.server = null;
895
- resolve();
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
- async function startTrackingServer(options) {
902
- const server = new TrackingServer();
903
- await server.start(options);
904
- return server;
805
+ function createTestContext() {
806
+ return {
807
+ resources: [],
808
+ variables: /* @__PURE__ */ new Map()
809
+ };
905
810
  }
906
- var TRACK_DIR = ".intellitester/track";
907
- var ACTIVE_TESTS_FILE = "ACTIVE_TESTS.json";
908
- var DEFAULT_STALE_MS = 2 * 60 * 60 * 1e3;
909
- var HEARTBEAT_MS = 15e3;
910
- var getTrackDir = (cwd, trackDir) => trackDir ? path4__default.resolve(cwd, trackDir) : path4__default.join(cwd, TRACK_DIR);
911
- var getActiveTestsPath = (cwd, trackDir) => path4__default.join(getTrackDir(cwd, trackDir), ACTIVE_TESTS_FILE);
912
- var loadActiveTests = async (cwd, trackDir) => {
913
- const filePath = getActiveTestsPath(cwd, trackDir);
914
- try {
915
- const content = await fs__default.readFile(filePath, "utf8");
916
- const parsed = JSON.parse(content);
917
- if (!parsed.sessions || typeof parsed.sessions !== "object") {
918
- throw new Error("Invalid ACTIVE_TESTS structure");
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
- return parsed;
921
- } catch {
857
+ }
858
+ return null;
859
+ }
860
+ function getBrowserLaunchOptions(options) {
861
+ const { headless, browser } = options;
862
+ if (browser === "chromium" || !browser) {
922
863
  return {
923
- version: 1,
924
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
925
- sessions: {}
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
- var saveActiveTests = async (cwd, state, trackDir) => {
930
- const filePath = getActiveTestsPath(cwd, trackDir);
931
- state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
932
- await fs__default.writeFile(filePath, JSON.stringify(state, null, 2), "utf8");
933
- };
934
- var isStale = (entry, staleMs) => {
935
- const last = new Date(entry.lastUpdated).getTime();
936
- if (Number.isNaN(last)) return true;
937
- return Date.now() - last > staleMs;
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
- }).filter((entry) => Boolean(entry));
950
- } catch {
951
- return [];
902
+ };
952
903
  }
953
- };
954
- var cleanupStaleSession = async (entry, cleanupConfig) => {
955
- if (!entry.providerConfig || !cleanupConfig?.provider) return;
956
- const resources = await readTrackedResources(entry.trackFile);
957
- if (resources.length === 0) return;
958
- try {
959
- const { handlers, typeMappings, provider } = await loadCleanupHandlers(cleanupConfig, entry.cwd);
960
- if (!provider) return;
961
- await executeCleanup(resources, handlers, typeMappings, {
962
- parallel: cleanupConfig.parallel ?? false,
963
- retries: cleanupConfig.retries ?? 3,
964
- sessionId: entry.sessionId,
965
- testStartTime: entry.startedAt,
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
- } catch {
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 cleanupOrphanedTrackFiles = async (cwd, state, trackDir) => {
975
- const dir = getTrackDir(cwd, trackDir);
976
- try {
977
- const files = await fs__default.readdir(dir);
978
- const activeFiles = new Set(Object.values(state.sessions).map((s) => path4__default.basename(s.trackFile)));
979
- for (const file of files) {
980
- if (!file.endsWith(".jsonl")) continue;
981
- if (activeFiles.has(file)) continue;
982
- const filePath = path4__default.join(dir, file);
983
- try {
984
- const stat2 = await fs__default.stat(filePath);
985
- const staleMs = Number(process.env.INTELLITESTER_STALE_TEST_MS ?? DEFAULT_STALE_MS);
986
- const isOld = Date.now() - stat2.mtimeMs > staleMs;
987
- const isEmpty = stat2.size === 0;
988
- if (isEmpty || isOld) {
989
- await fs__default.rm(filePath, { force: true });
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
- } catch {
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 pruneStaleTests = async (cwd, cleanupConfig, trackDir) => {
998
- const state = await loadActiveTests(cwd, trackDir);
999
- const staleMs = Number(process.env.INTELLITESTER_STALE_TEST_MS ?? DEFAULT_STALE_MS);
1000
- let changed = false;
1001
- for (const [sessionId, entry] of Object.entries(state.sessions)) {
1002
- let missingFile = false;
1003
- let isEmpty = false;
1004
- try {
1005
- const stat2 = await fs__default.stat(entry.trackFile);
1006
- isEmpty = stat2.size === 0;
1007
- } catch {
1008
- missingFile = true;
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
- if (!missingFile && !isEmpty && !isStale(entry, staleMs)) continue;
1011
- changed = true;
1012
- await cleanupStaleSession(entry, cleanupConfig);
1013
- try {
1014
- await fs__default.rm(entry.trackFile, { force: true });
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
- delete state.sessions[sessionId];
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
- async function initFileTracking(options) {
1025
- const cwd = options.cwd ?? process.cwd();
1026
- const trackDir = options.trackDir;
1027
- await fs__default.mkdir(getTrackDir(cwd, trackDir), { recursive: true });
1028
- await pruneStaleTests(cwd, options.cleanupConfig, trackDir);
1029
- const trackFile = path4__default.join(getTrackDir(cwd, trackDir), `TEST_SESSION_${options.sessionId}.jsonl`);
1030
- await fs__default.writeFile(trackFile, "", "utf8");
1031
- const state = await loadActiveTests(cwd, trackDir);
1032
- state.sessions[options.sessionId] = {
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
- async function mergeFileTrackedResources(trackFile, target, allowedTypes) {
1065
- const entries = await readTrackedResources(trackFile);
1066
- for (const entry of entries) {
1067
- const resource = entry;
1068
- if (!resource.type || !resource.id) continue;
1069
- if (allowedTypes && !allowedTypes.has(String(resource.type))) continue;
1070
- const exists = target.resources.some(
1071
- (existing) => existing.type === resource.type && existing.id === resource.id
1072
- );
1073
- if (!exists) {
1074
- target.resources.push(resource);
1075
- }
1076
- if (resource.type === "user") {
1077
- if (!target.userId && typeof resource.id === "string") {
1078
- target.userId = resource.id;
1079
- }
1080
- const email = resource.email;
1081
- if (!target.userEmail && typeof email === "string") {
1082
- target.userEmail = email;
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
- const trackingServer = new TrackingServer();
2166
- await trackingServer.start();
2167
- process.env.INTELLITESTER_SESSION_ID = sessionId;
2168
- process.env.INTELLITESTER_TRACK_URL = `http://localhost:${trackingServer.port}`;
2169
- const fileTracking = await initFileTracking({
2170
- sessionId,
2171
- trackDir: options.trackDir,
2172
- cleanupConfig: test.config?.appwrite?.cleanup ? {
2173
- provider: "appwrite",
2174
- scanUntracked: true,
2175
- appwrite: {
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
- cleanupOnFailure: test.config.appwrite.cleanupOnFailure
2180
- }
2181
- } : void 0,
2182
- providerConfig: test.config?.appwrite ? {
2183
- provider: "appwrite",
2184
- endpoint: test.config.appwrite.endpoint,
2185
- projectId: test.config.appwrite.projectId,
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 webServerConfig = requiresTrackingEnv ? { ...options.webServer, reuseExistingServer: false } : options.webServer;
2195
- if (requiresTrackingEnv && options.webServer.reuseExistingServer !== false) {
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
- trackingServer.stop();
2202
- webServerManager.kill();
2203
- void fileTracking.stop();
2204
- delete process.env.INTELLITESTER_TRACK_FILE;
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.getResources(sessionId);
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
- await fileTracking.stop();
2556
- delete process.env.INTELLITESTER_TRACK_FILE;
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
- try {
3515
- trackingServer = await startTrackingServer({ port: 0 });
3516
- console.log(`Tracking server started on port ${trackingServer.port}`);
3517
- } catch (error) {
3518
- console.warn("Failed to start tracking server:", error);
3519
- }
3520
- if (trackingServer) {
3521
- process.env.INTELLITESTER_SESSION_ID = sessionId;
3522
- process.env.INTELLITESTER_TRACK_URL = `http://localhost:${trackingServer.port}`;
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
- if (webServerConfig) {
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 effectiveWebServerConfig = requiresTrackingEnv ? { ...webServerConfig, reuseExistingServer: false } : webServerConfig;
3544
- if (requiresTrackingEnv && webServerConfig.reuseExistingServer !== false) {
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
- ...effectiveWebServerConfig,
3549
- workdir: path4__default.resolve(serverCwd, effectiveWebServerConfig.workdir ?? effectiveWebServerConfig.cwd ?? ".")
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
- webServerManager.kill();
3560
- if (trackingServer) await trackingServer.stop();
3561
- await fileTracking.stop();
3562
- delete process.env.INTELLITESTER_TRACK_FILE;
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
- await mergeFileTrackedResources(fileTracking.trackFile, executionContext.appwriteContext);
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
- await webServerManager.stop();
3745
- if (trackingServer) {
3746
- await trackingServer.stop();
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-WNSVKXTD.js.map
3757
- //# sourceMappingURL=chunk-WNSVKXTD.js.map
3799
+ //# sourceMappingURL=chunk-PGOHFXEG.js.map
3800
+ //# sourceMappingURL=chunk-PGOHFXEG.js.map