@hasna/testers 0.0.46 → 0.0.48

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/cli/index.js CHANGED
@@ -27025,12 +27025,60 @@ function listTestingWorkflows(filter) {
27025
27025
  const rows = db2.query(`SELECT * FROM testing_workflows${where} ORDER BY created_at DESC`).all(...params);
27026
27026
  return rows.map(workflowFromRow);
27027
27027
  }
27028
+ function updateTestingWorkflow(id, input) {
27029
+ const existing = getTestingWorkflow(id);
27030
+ if (!existing)
27031
+ throw new Error(`Testing workflow not found: ${id}`);
27032
+ const fields = [];
27033
+ const values = [];
27034
+ if (input.name !== undefined) {
27035
+ fields.push("name = ?");
27036
+ values.push(input.name);
27037
+ }
27038
+ if (input.description !== undefined) {
27039
+ fields.push("description = ?");
27040
+ values.push(input.description);
27041
+ }
27042
+ if (input.scenarioFilter !== undefined) {
27043
+ fields.push("scenario_filter = ?");
27044
+ values.push(JSON.stringify(normalizeFilter(input.scenarioFilter)));
27045
+ }
27046
+ if (input.personaIds !== undefined) {
27047
+ fields.push("persona_ids = ?");
27048
+ values.push(JSON.stringify(input.personaIds));
27049
+ }
27050
+ if (input.goal !== undefined) {
27051
+ fields.push("goal = ?");
27052
+ values.push(JSON.stringify(normalizeGoal(input.goal)));
27053
+ }
27054
+ if (input.execution !== undefined) {
27055
+ fields.push("execution = ?");
27056
+ values.push(JSON.stringify(normalizeExecution(input.execution)));
27057
+ }
27058
+ if (input.settings !== undefined) {
27059
+ fields.push("settings = ?");
27060
+ values.push(JSON.stringify(input.settings));
27061
+ }
27062
+ if (input.enabled !== undefined) {
27063
+ fields.push("enabled = ?");
27064
+ values.push(input.enabled ? 1 : 0);
27065
+ }
27066
+ if (fields.length === 0)
27067
+ return existing;
27068
+ fields.push("updated_at = ?");
27069
+ values.push(now(), existing.id);
27070
+ db2().query(`UPDATE testing_workflows SET ${fields.join(", ")} WHERE id = ?`).run(...values);
27071
+ return getTestingWorkflow(existing.id);
27072
+ }
27028
27073
  function deleteTestingWorkflow(id) {
27029
27074
  const existing = getTestingWorkflow(id);
27030
27075
  if (!existing)
27031
27076
  return false;
27032
27077
  return getDatabase().query("DELETE FROM testing_workflows WHERE id = ?").run(existing.id).changes > 0;
27033
27078
  }
27079
+ function db2() {
27080
+ return getDatabase();
27081
+ }
27034
27082
  var DEFAULT_EXECUTION;
27035
27083
  var init_workflows = __esm(() => {
27036
27084
  init_types();
@@ -27262,7 +27310,7 @@ async function runViaSandbox(plan, dependencies) {
27262
27310
  workflowId: plan.workflow.id,
27263
27311
  workflowName: plan.workflow.name
27264
27312
  },
27265
- sandboxEnvVars: plan.sandbox.env,
27313
+ sandboxEnvVars: resolveSandboxEnv(plan.sandbox.env),
27266
27314
  cleanup: plan.sandbox.cleanup,
27267
27315
  upload: {
27268
27316
  localDir: bundle.localDir,
@@ -27288,6 +27336,26 @@ async function runViaSandbox(plan, dependencies) {
27288
27336
  bundle.cleanup?.();
27289
27337
  }
27290
27338
  }
27339
+ function resolveSandboxEnv(env) {
27340
+ if (!env || Object.keys(env).length === 0)
27341
+ return;
27342
+ const resolved = {};
27343
+ for (const [key, value] of Object.entries(env)) {
27344
+ if (value.startsWith("$?")) {
27345
+ const optionalName = value.slice(2).trim();
27346
+ const optionalValue = optionalName ? process.env[optionalName] : undefined;
27347
+ if (optionalValue !== undefined)
27348
+ resolved[key] = optionalValue;
27349
+ continue;
27350
+ }
27351
+ const resolvedValue = resolveCredential(value);
27352
+ if (resolvedValue === null) {
27353
+ throw new Error(`Missing sandbox env value for ${key}`);
27354
+ }
27355
+ resolved[key] = resolvedValue;
27356
+ }
27357
+ return resolved;
27358
+ }
27291
27359
  async function resolveSandboxesRuntime(dependencies) {
27292
27360
  if (dependencies.sandboxes)
27293
27361
  return dependencies.sandboxes;
@@ -27305,6 +27373,7 @@ var init_workflow_runner = __esm(() => {
27305
27373
  init_workflows();
27306
27374
  init_personas();
27307
27375
  init_runner();
27376
+ init_secrets_resolver();
27308
27377
  APP_SOURCE_EXCLUDES = [
27309
27378
  "node_modules",
27310
27379
  ".git",
@@ -27312,6 +27381,8 @@ var init_workflow_runner = __esm(() => {
27312
27381
  ".next",
27313
27382
  ".turbo",
27314
27383
  ".cache",
27384
+ ".env",
27385
+ ".env.*",
27315
27386
  ".venv",
27316
27387
  "__pycache__"
27317
27388
  ];
@@ -27465,46 +27536,46 @@ __export(exports_sessions, {
27465
27536
  countSessions: () => countSessions
27466
27537
  });
27467
27538
  function createSession(input) {
27468
- const db2 = getDatabase();
27539
+ const db3 = getDatabase();
27469
27540
  const id = input.sessionId ?? uuid();
27470
27541
  const timestamp = now();
27471
- db2.query(`
27542
+ db3.query(`
27472
27543
  INSERT INTO sessions (id, tab_id, url, title, entries, entry_count, error_count, console_count, nav_count, status, start_time, end_time, created_at)
27473
27544
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
27474
27545
  `).run(id, input.tabId, input.url ?? null, input.title ?? null, input.entries, input.entryCount, input.errorCount ?? 0, input.consoleCount ?? 0, input.navCount ?? 0, input.status, input.startTime, input.endTime ?? null, timestamp);
27475
27546
  return getSession(id);
27476
27547
  }
27477
27548
  function getSession(id) {
27478
- const db2 = getDatabase();
27479
- let row = db2.query("SELECT * FROM sessions WHERE id = ?").get(id);
27549
+ const db3 = getDatabase();
27550
+ let row = db3.query("SELECT * FROM sessions WHERE id = ?").get(id);
27480
27551
  if (row)
27481
27552
  return sessionFromRow(row);
27482
27553
  const fullId = resolvePartialId("sessions", id);
27483
27554
  if (fullId) {
27484
- row = db2.query("SELECT * FROM sessions WHERE id = ?").get(fullId);
27555
+ row = db3.query("SELECT * FROM sessions WHERE id = ?").get(fullId);
27485
27556
  if (row)
27486
27557
  return sessionFromRow(row);
27487
27558
  }
27488
27559
  return null;
27489
27560
  }
27490
27561
  function listSessions(limit = 50, offset = 0) {
27491
- const db2 = getDatabase();
27492
- const rows = db2.query("SELECT * FROM sessions ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
27562
+ const db3 = getDatabase();
27563
+ const rows = db3.query("SELECT * FROM sessions ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
27493
27564
  return rows.map(sessionFromRow);
27494
27565
  }
27495
27566
  function deleteSession(id) {
27496
- const db2 = getDatabase();
27497
- const result = db2.query("DELETE FROM sessions WHERE id = ?").run(id);
27567
+ const db3 = getDatabase();
27568
+ const result = db3.query("DELETE FROM sessions WHERE id = ?").run(id);
27498
27569
  return result.changes > 0;
27499
27570
  }
27500
27571
  function searchSessions(query, limit = 20) {
27501
- const db2 = getDatabase();
27502
- const rows = db2.query("SELECT * FROM sessions WHERE url LIKE ? OR title LIKE ? ORDER BY created_at DESC LIMIT ?").all(`%${query}%`, `%${query}%`, limit);
27572
+ const db3 = getDatabase();
27573
+ const rows = db3.query("SELECT * FROM sessions WHERE url LIKE ? OR title LIKE ? ORDER BY created_at DESC LIMIT ?").all(`%${query}%`, `%${query}%`, limit);
27503
27574
  return rows.map(sessionFromRow);
27504
27575
  }
27505
27576
  function countSessions() {
27506
- const db2 = getDatabase();
27507
- const row = db2.query("SELECT COUNT(*) as count FROM sessions").get();
27577
+ const db3 = getDatabase();
27578
+ const row = db3.query("SELECT COUNT(*) as count FROM sessions").get();
27508
27579
  return row.count;
27509
27580
  }
27510
27581
  function sessionFromRow(row) {
@@ -28032,8 +28103,8 @@ function customRandom(alphabet, defaultSize, getRandom) {
28032
28103
  function customAlphabet(alphabet, size = 21) {
28033
28104
  return customRandom(alphabet, size, random);
28034
28105
  }
28035
- function runMigrations(db2) {
28036
- db2.run(`
28106
+ function runMigrations(db3) {
28107
+ db3.run(`
28037
28108
  CREATE TABLE IF NOT EXISTS _migrations (
28038
28109
  id INTEGER PRIMARY KEY,
28039
28110
  applied_at TEXT NOT NULL DEFAULT (datetime('now'))
@@ -28041,9 +28112,9 @@ function runMigrations(db2) {
28041
28112
  `);
28042
28113
  for (let i = 0;i < MIGRATIONS2.length; i++) {
28043
28114
  const migrationId = i + 1;
28044
- const exists = db2.query("SELECT id FROM _migrations WHERE id = ?").get(migrationId);
28115
+ const exists = db3.query("SELECT id FROM _migrations WHERE id = ?").get(migrationId);
28045
28116
  if (!exists) {
28046
- db2.run(MIGRATIONS2[i]);
28117
+ db3.run(MIGRATIONS2[i]);
28047
28118
  }
28048
28119
  }
28049
28120
  }
@@ -28068,11 +28139,11 @@ function ensureDir2(filePath) {
28068
28139
  function getDatabase2(path) {
28069
28140
  if (path) {
28070
28141
  ensureDir2(path);
28071
- const db2 = new Database4(path);
28072
- db2.run("PRAGMA journal_mode=WAL");
28073
- db2.run("PRAGMA foreign_keys=ON");
28074
- runMigrations(db2);
28075
- return db2;
28142
+ const db3 = new Database4(path);
28143
+ db3.run("PRAGMA journal_mode=WAL");
28144
+ db3.run("PRAGMA foreign_keys=ON");
28145
+ runMigrations(db3);
28146
+ return db3;
28076
28147
  }
28077
28148
  if (!_db) {
28078
28149
  const dbPath = getDbPath2();
@@ -28140,8 +28211,8 @@ function rowToWorkdir(row) {
28140
28211
  function getMachineId() {
28141
28212
  return process.env["HOSTNAME"] || hostname();
28142
28213
  }
28143
- function addWorkdir(input, db2) {
28144
- const d = db2 || getDatabase2();
28214
+ function addWorkdir(input, db3) {
28215
+ const d = db3 || getDatabase2();
28145
28216
  const project = getProject2(input.project_id, d);
28146
28217
  if (!project)
28147
28218
  throw new ProjectNotFoundError(input.project_id);
@@ -28158,18 +28229,18 @@ function addWorkdir(input, db2) {
28158
28229
  ON CONFLICT(project_id, path) DO UPDATE SET label = excluded.label, machine_id = excluded.machine_id`, [id, input.project_id, input.path, machineId, input.label ?? "main", isPrimary, ts]);
28159
28230
  return getWorkdir(input.project_id, input.path, d);
28160
28231
  }
28161
- function getWorkdir(projectId, path, db2) {
28162
- const d = db2 || getDatabase2();
28232
+ function getWorkdir(projectId, path, db3) {
28233
+ const d = db3 || getDatabase2();
28163
28234
  const row = d.query("SELECT * FROM project_workdirs WHERE project_id = ? AND path = ?").get(projectId, path);
28164
28235
  return row ? rowToWorkdir(row) : null;
28165
28236
  }
28166
- function listWorkdirs(projectId, db2) {
28167
- const d = db2 || getDatabase2();
28237
+ function listWorkdirs(projectId, db3) {
28238
+ const d = db3 || getDatabase2();
28168
28239
  const rows = d.query("SELECT * FROM project_workdirs WHERE project_id = ? ORDER BY is_primary DESC, created_at ASC").all(projectId);
28169
28240
  return rows.map(rowToWorkdir);
28170
28241
  }
28171
- function removeWorkdir(projectId, path, db2) {
28172
- const d = db2 || getDatabase2();
28242
+ function removeWorkdir(projectId, path, db3) {
28243
+ const d = db3 || getDatabase2();
28173
28244
  d.run("DELETE FROM project_workdirs WHERE project_id = ? AND path = ?", [projectId, path]);
28174
28245
  }
28175
28246
  function generateProjectId() {
@@ -28187,11 +28258,11 @@ function scaffoldProject(path) {
28187
28258
  mkdirSync32(join42(path, dir), { recursive: true });
28188
28259
  }
28189
28260
  }
28190
- function ensureUniqueSlug(base, db2, excludeId) {
28261
+ function ensureUniqueSlug(base, db3, excludeId) {
28191
28262
  let candidate = base;
28192
28263
  let suffix = 1;
28193
28264
  while (true) {
28194
- const row = db2.query("SELECT id FROM projects WHERE slug = ?").get(candidate);
28265
+ const row = db3.query("SELECT id FROM projects WHERE slug = ?").get(candidate);
28195
28266
  if (!row || row.id === excludeId)
28196
28267
  return candidate;
28197
28268
  suffix++;
@@ -28207,8 +28278,8 @@ function rowToProject(row) {
28207
28278
  last_opened_at: row.last_opened_at ?? null
28208
28279
  };
28209
28280
  }
28210
- function createProject2(input, db2) {
28211
- const d = db2 || getDatabase2();
28281
+ function createProject2(input, db3) {
28282
+ const d = db3 || getDatabase2();
28212
28283
  const id = generateProjectId();
28213
28284
  const ts = now2();
28214
28285
  const baseSlug = input.slug || slugify2(input.name);
@@ -28243,23 +28314,23 @@ function createProject2(input, db2) {
28243
28314
  }
28244
28315
  return getProject2(id, d);
28245
28316
  }
28246
- function getProject2(id, db2) {
28247
- const d = db2 || getDatabase2();
28317
+ function getProject2(id, db3) {
28318
+ const d = db3 || getDatabase2();
28248
28319
  const row = d.query("SELECT * FROM projects WHERE id = ?").get(id);
28249
28320
  return row ? rowToProject(row) : null;
28250
28321
  }
28251
- function getProjectBySlug(slug, db2) {
28252
- const d = db2 || getDatabase2();
28322
+ function getProjectBySlug(slug, db3) {
28323
+ const d = db3 || getDatabase2();
28253
28324
  const row = d.query("SELECT * FROM projects WHERE slug = ?").get(slug);
28254
28325
  return row ? rowToProject(row) : null;
28255
28326
  }
28256
- function getProjectByPath2(path, db2) {
28257
- const d = db2 || getDatabase2();
28327
+ function getProjectByPath2(path, db3) {
28328
+ const d = db3 || getDatabase2();
28258
28329
  const row = d.query("SELECT * FROM projects WHERE path = ?").get(path);
28259
28330
  return row ? rowToProject(row) : null;
28260
28331
  }
28261
- function listProjects2(filter = {}, db2) {
28262
- const d = db2 || getDatabase2();
28332
+ function listProjects2(filter = {}, db3) {
28333
+ const d = db3 || getDatabase2();
28263
28334
  const conditions = [];
28264
28335
  const params = [];
28265
28336
  if (filter.status) {
@@ -28278,8 +28349,8 @@ function listProjects2(filter = {}, db2) {
28278
28349
  }
28279
28350
  return rows.map(rowToProject);
28280
28351
  }
28281
- function updateProject(id, input, db2) {
28282
- const d = db2 || getDatabase2();
28352
+ function updateProject(id, input, db3) {
28353
+ const d = db3 || getDatabase2();
28283
28354
  const project = getProject2(id, d);
28284
28355
  if (!project)
28285
28356
  throw new ProjectNotFoundError(id);
@@ -28328,8 +28399,8 @@ function updateProject(id, input, db2) {
28328
28399
  d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
28329
28400
  return getProject2(id, d);
28330
28401
  }
28331
- function setIntegrations(id, integrations, db2) {
28332
- const d = db2 || getDatabase2();
28402
+ function setIntegrations(id, integrations, db3) {
28403
+ const d = db3 || getDatabase2();
28333
28404
  const project = getProject2(id, d);
28334
28405
  if (!project)
28335
28406
  throw new ProjectNotFoundError(id);
@@ -28349,16 +28420,16 @@ function setIntegrations(id, integrations, db2) {
28349
28420
  } catch {}
28350
28421
  return getProject2(id, d);
28351
28422
  }
28352
- function archiveProject(id, db2) {
28353
- const d = db2 || getDatabase2();
28423
+ function archiveProject(id, db3) {
28424
+ const d = db3 || getDatabase2();
28354
28425
  const project = getProject2(id, d);
28355
28426
  if (!project)
28356
28427
  throw new ProjectNotFoundError(id);
28357
28428
  d.run("UPDATE projects SET status = 'archived', updated_at = ? WHERE id = ?", [now2(), id]);
28358
28429
  return getProject2(id, d);
28359
28430
  }
28360
- function unarchiveProject(id, db2) {
28361
- const d = db2 || getDatabase2();
28431
+ function unarchiveProject(id, db3) {
28432
+ const d = db3 || getDatabase2();
28362
28433
  const project = getProject2(id, d);
28363
28434
  if (!project)
28364
28435
  throw new ProjectNotFoundError(id);
@@ -28375,8 +28446,8 @@ function levenshtein(a, b) {
28375
28446
  dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
28376
28447
  return dp[m][n];
28377
28448
  }
28378
- function resolveProject(idOrSlug, db2) {
28379
- const d = db2 || getDatabase2();
28449
+ function resolveProject(idOrSlug, db3) {
28450
+ const d = db3 || getDatabase2();
28380
28451
  let project = getProject2(idOrSlug, d);
28381
28452
  if (project)
28382
28453
  return project;
@@ -28399,22 +28470,22 @@ function resolveProject(idOrSlug, db2) {
28399
28470
  function syncRowToLog(row) {
28400
28471
  return { ...row, direction: row.direction, status: row.status };
28401
28472
  }
28402
- function startSyncLog(projectId, direction, db2) {
28403
- const d = db2 || getDatabase2();
28473
+ function startSyncLog(projectId, direction, db3) {
28474
+ const d = db3 || getDatabase2();
28404
28475
  const id = uuid2();
28405
28476
  const ts = now2();
28406
28477
  d.run("INSERT INTO sync_log (id, project_id, direction, status, started_at) VALUES (?, ?, ?, 'running', ?)", [id, projectId, direction, ts]);
28407
28478
  return d.query("SELECT * FROM sync_log WHERE id = ?").get(id);
28408
28479
  }
28409
- function completeSyncLog(id, result, db2) {
28410
- const d = db2 || getDatabase2();
28480
+ function completeSyncLog(id, result, db3) {
28481
+ const d = db3 || getDatabase2();
28411
28482
  const status = result.error ? "failed" : "completed";
28412
28483
  d.run("UPDATE sync_log SET status = ?, files_synced = ?, bytes = ?, error = ?, completed_at = ? WHERE id = ?", [status, result.files_synced ?? 0, result.bytes ?? 0, result.error ?? null, now2(), id]);
28413
28484
  const row = d.query("SELECT * FROM sync_log WHERE id = ?").get(id);
28414
28485
  return syncRowToLog(row);
28415
28486
  }
28416
- function listSyncLogs(projectId, limit = 20, db2) {
28417
- const d = db2 || getDatabase2();
28487
+ function listSyncLogs(projectId, limit = 20, db3) {
28488
+ const d = db3 || getDatabase2();
28418
28489
  const rows = d.query("SELECT * FROM sync_log WHERE project_id = ? ORDER BY started_at DESC LIMIT ?").all(projectId, limit);
28419
28490
  return rows.map(syncRowToLog);
28420
28491
  }
@@ -58782,7 +58853,7 @@ function buildPeriod(days) {
58782
58853
  return { periodStart, periodEnd };
58783
58854
  }
58784
58855
  async function collectComplianceData(options) {
58785
- const db2 = getDatabase();
58856
+ const db3 = getDatabase();
58786
58857
  const runsInPeriod = listRuns({
58787
58858
  projectId: options.projectId,
58788
58859
  limit: 1e4
@@ -58797,13 +58868,13 @@ async function collectComplianceData(options) {
58797
58868
  scanConditions.push("project_id = ?");
58798
58869
  scanParams.push(options.projectId);
58799
58870
  }
58800
- const scanIssues = db2.query(`SELECT type, severity FROM scan_issues WHERE ${scanConditions.join(" AND ")}`).all(...scanParams);
58871
+ const scanIssues = db3.query(`SELECT type, severity FROM scan_issues WHERE ${scanConditions.join(" AND ")}`).all(...scanParams);
58801
58872
  const injectionProbesRun = scanIssues.filter((i2) => i2.type === "injection" || i2.type === "sql_injection" || i2.type === "xss").length;
58802
58873
  const injectionVulnsFound = scanIssues.filter((i2) => (i2.type === "injection" || i2.type === "xss") && (i2.severity === "high" || i2.severity === "critical")).length;
58803
58874
  const piiLeaksDetected = scanIssues.filter((i2) => i2.type === "pii" || i2.type === "pii_leak").length;
58804
58875
  let goldenAnswerDriftEvents = 0;
58805
58876
  try {
58806
- const driftRows = db2.query(`SELECT COUNT(*) as count FROM golden_check_results WHERE drift_detected = 1 AND created_at >= ? AND created_at <= ?`).get(options.periodStart, options.periodEnd);
58877
+ const driftRows = db3.query(`SELECT COUNT(*) as count FROM golden_check_results WHERE drift_detected = 1 AND created_at >= ? AND created_at <= ?`).get(options.periodStart, options.periodEnd);
58807
58878
  goldenAnswerDriftEvents = driftRows?.count ?? 0;
58808
58879
  } catch {
58809
58880
  goldenAnswerDriftEvents = 0;
@@ -58815,7 +58886,7 @@ async function collectComplianceData(options) {
58815
58886
  let flakyScenarioCount = 0;
58816
58887
  if (runIds.length > 0) {
58817
58888
  const placeholders = runIds.map(() => "?").join(", ");
58818
- const evalResults = db2.query(`SELECT r.status, r.metadata FROM results r
58889
+ const evalResults = db3.query(`SELECT r.status, r.metadata FROM results r
58819
58890
  JOIN scenarios s ON r.scenario_id = s.id
58820
58891
  WHERE r.run_id IN (${placeholders}) AND s.scenario_type = 'eval'`).all(...runIds);
58821
58892
  evalScenariosRun = evalResults.length;
@@ -58828,9 +58899,9 @@ async function collectComplianceData(options) {
58828
58899
  totalEvalScore += er.status === "passed" ? 1 : 0;
58829
58900
  }
58830
58901
  }
58831
- const a11yRows = db2.query(`SELECT COUNT(*) as count FROM scan_issues WHERE type = 'a11y' AND severity = 'critical' AND first_seen_at >= ? AND first_seen_at <= ?`).get(options.periodStart, options.periodEnd);
58902
+ const a11yRows = db3.query(`SELECT COUNT(*) as count FROM scan_issues WHERE type = 'a11y' AND severity = 'critical' AND first_seen_at >= ? AND first_seen_at <= ?`).get(options.periodStart, options.periodEnd);
58832
58903
  a11yViolationsCritical = a11yRows?.count ?? 0;
58833
- const flakyRows = db2.query(`SELECT COUNT(DISTINCT scenario_id) as count FROM results WHERE run_id IN (${placeholders}) AND status = 'flaky'`).get(...runIds);
58904
+ const flakyRows = db3.query(`SELECT COUNT(DISTINCT scenario_id) as count FROM results WHERE run_id IN (${placeholders}) AND status = 'flaky'`).get(...runIds);
58834
58905
  flakyScenarioCount = flakyRows?.count ?? 0;
58835
58906
  }
58836
58907
  const averageEvalScore = evalScenariosRun > 0 ? totalEvalScore / evalScenariosRun : 1;
@@ -59649,49 +59720,49 @@ __export(exports_agents, {
59649
59720
  getAgent: () => getAgent
59650
59721
  });
59651
59722
  function registerAgent(input) {
59652
- const db2 = getDatabase();
59653
- const existing = db2.query("SELECT * FROM agents WHERE name = ?").get(input.name);
59723
+ const db3 = getDatabase();
59724
+ const existing = db3.query("SELECT * FROM agents WHERE name = ?").get(input.name);
59654
59725
  if (existing) {
59655
- db2.query("UPDATE agents SET last_seen_at = ? WHERE id = ?").run(now(), existing.id);
59726
+ db3.query("UPDATE agents SET last_seen_at = ? WHERE id = ?").run(now(), existing.id);
59656
59727
  return getAgent(existing.id);
59657
59728
  }
59658
59729
  const id = uuid();
59659
59730
  const timestamp = now();
59660
- db2.query(`
59731
+ db3.query(`
59661
59732
  INSERT INTO agents (id, name, description, role, metadata, created_at, last_seen_at)
59662
59733
  VALUES (?, ?, ?, ?, '{}', ?, ?)
59663
59734
  `).run(id, input.name, input.description ?? null, input.role ?? null, timestamp, timestamp);
59664
59735
  return getAgent(id);
59665
59736
  }
59666
59737
  function getAgent(id) {
59667
- const db2 = getDatabase();
59668
- const row = db2.query("SELECT * FROM agents WHERE id = ?").get(id);
59738
+ const db3 = getDatabase();
59739
+ const row = db3.query("SELECT * FROM agents WHERE id = ?").get(id);
59669
59740
  return row ? agentFromRow(row) : null;
59670
59741
  }
59671
59742
  function getAgentByName(name) {
59672
- const db2 = getDatabase();
59673
- const row = db2.query("SELECT * FROM agents WHERE name = ?").get(name);
59743
+ const db3 = getDatabase();
59744
+ const row = db3.query("SELECT * FROM agents WHERE name = ?").get(name);
59674
59745
  return row ? agentFromRow(row) : null;
59675
59746
  }
59676
59747
  function listAgents() {
59677
- const db2 = getDatabase();
59678
- const rows = db2.query("SELECT * FROM agents ORDER BY created_at DESC").all();
59748
+ const db3 = getDatabase();
59749
+ const rows = db3.query("SELECT * FROM agents ORDER BY created_at DESC").all();
59679
59750
  return rows.map(agentFromRow);
59680
59751
  }
59681
59752
  function heartbeatAgent(id) {
59682
- const db2 = getDatabase();
59683
- const affected = db2.query("UPDATE agents SET last_seen_at = ? WHERE id = ?").run(now(), id);
59753
+ const db3 = getDatabase();
59754
+ const affected = db3.query("UPDATE agents SET last_seen_at = ? WHERE id = ?").run(now(), id);
59684
59755
  if (affected.changes === 0)
59685
59756
  return null;
59686
59757
  return getAgent(id);
59687
59758
  }
59688
59759
  function setAgentFocus(id, scenarioId) {
59689
- const db2 = getDatabase();
59760
+ const db3 = getDatabase();
59690
59761
  const agent = getAgent(id);
59691
59762
  if (!agent)
59692
59763
  return null;
59693
59764
  const metadata = { ...agent.metadata ?? {}, focus: scenarioId };
59694
- db2.query("UPDATE agents SET metadata = ?, last_seen_at = ? WHERE id = ?").run(JSON.stringify(metadata), now(), id);
59765
+ db3.query("UPDATE agents SET metadata = ?, last_seen_at = ? WHERE id = ?").run(JSON.stringify(metadata), now(), id);
59695
59766
  return getAgent(id);
59696
59767
  }
59697
59768
  var init_agents = __esm(() => {
@@ -94075,28 +94146,28 @@ function checkResultFromRow(row) {
94075
94146
  };
94076
94147
  }
94077
94148
  function createGoldenAnswer(input) {
94078
- const db2 = getDatabase();
94149
+ const db3 = getDatabase();
94079
94150
  const id = uuid();
94080
94151
  const short_id = shortUuid();
94081
94152
  const timestamp = now();
94082
- db2.query(`
94153
+ db3.query(`
94083
94154
  INSERT INTO golden_answers (id, short_id, project_id, question, golden_answer, constraints, endpoint, judge_model, enabled, created_at, updated_at)
94084
94155
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
94085
94156
  `).run(id, short_id, input.projectId ?? null, input.question, input.goldenAnswer, JSON.stringify(input.constraints ?? []), input.endpoint, input.judgeModel ?? null, input.enabled === false ? 0 : 1, timestamp, timestamp);
94086
94157
  return getGoldenAnswer(id);
94087
94158
  }
94088
94159
  function getGoldenAnswer(id) {
94089
- const db2 = getDatabase();
94090
- let row = db2.query("SELECT * FROM golden_answers WHERE id = ?").get(id);
94160
+ const db3 = getDatabase();
94161
+ let row = db3.query("SELECT * FROM golden_answers WHERE id = ?").get(id);
94091
94162
  if (row)
94092
94163
  return goldenFromRow(row);
94093
- row = db2.query("SELECT * FROM golden_answers WHERE short_id = ?").get(id);
94164
+ row = db3.query("SELECT * FROM golden_answers WHERE short_id = ?").get(id);
94094
94165
  if (row)
94095
94166
  return goldenFromRow(row);
94096
94167
  return null;
94097
94168
  }
94098
94169
  function listGoldenAnswers(filter2) {
94099
- const db2 = getDatabase();
94170
+ const db3 = getDatabase();
94100
94171
  const conditions = [];
94101
94172
  const params = [];
94102
94173
  if (filter2?.projectId) {
@@ -94112,11 +94183,11 @@ function listGoldenAnswers(filter2) {
94112
94183
  sql += " WHERE " + conditions.join(" AND ");
94113
94184
  }
94114
94185
  sql += " ORDER BY created_at DESC";
94115
- const rows = db2.query(sql).all(...params);
94186
+ const rows = db3.query(sql).all(...params);
94116
94187
  return rows.map(goldenFromRow);
94117
94188
  }
94118
94189
  function updateGoldenAnswer(id, input) {
94119
- const db2 = getDatabase();
94190
+ const db3 = getDatabase();
94120
94191
  const timestamp = now();
94121
94192
  const sets = ["updated_at = ?"];
94122
94193
  const params = [timestamp];
@@ -94145,26 +94216,26 @@ function updateGoldenAnswer(id, input) {
94145
94216
  params.push(input.enabled ? 1 : 0);
94146
94217
  }
94147
94218
  params.push(id);
94148
- db2.query(`UPDATE golden_answers SET ${sets.join(", ")} WHERE id = ? OR short_id = ?`).run(...params, id);
94219
+ db3.query(`UPDATE golden_answers SET ${sets.join(", ")} WHERE id = ? OR short_id = ?`).run(...params, id);
94149
94220
  return getGoldenAnswer(id);
94150
94221
  }
94151
94222
  function deleteGoldenAnswer(id) {
94152
- const db2 = getDatabase();
94153
- const result = db2.query("DELETE FROM golden_answers WHERE id = ? OR short_id = ?").run(id, id);
94223
+ const db3 = getDatabase();
94224
+ const result = db3.query("DELETE FROM golden_answers WHERE id = ? OR short_id = ?").run(id, id);
94154
94225
  return (result.changes ?? 0) > 0;
94155
94226
  }
94156
94227
  function createGoldenCheckResult(input) {
94157
- const db2 = getDatabase();
94228
+ const db3 = getDatabase();
94158
94229
  const id = uuid();
94159
94230
  const timestamp = now();
94160
- db2.query(`
94231
+ db3.query(`
94161
94232
  INSERT INTO golden_check_results (id, golden_id, response, similarity_score, passed, drift_detected, judge_model, provider, created_at)
94162
94233
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
94163
94234
  `).run(id, input.goldenId, input.response, input.similarityScore ?? null, input.passed ? 1 : 0, input.driftDetected ? 1 : 0, input.judgeModel ?? null, input.provider ?? null, timestamp);
94164
94235
  return listGoldenCheckResults(input.goldenId, { limit: 1 })[0];
94165
94236
  }
94166
94237
  function listGoldenCheckResults(goldenId, options) {
94167
- const db2 = getDatabase();
94238
+ const db3 = getDatabase();
94168
94239
  const params = [goldenId];
94169
94240
  let sql = "SELECT * FROM golden_check_results WHERE golden_id = ?";
94170
94241
  if (options?.since) {
@@ -94176,7 +94247,7 @@ function listGoldenCheckResults(goldenId, options) {
94176
94247
  sql += " LIMIT ?";
94177
94248
  params.push(options.limit);
94178
94249
  }
94179
- const rows = db2.query(sql).all(...params);
94250
+ const rows = db3.query(sql).all(...params);
94180
94251
  return rows.map(checkResultFromRow);
94181
94252
  }
94182
94253
  function getLatestGoldenCheckResult(goldenId) {
@@ -94465,7 +94536,7 @@ import chalk6 from "chalk";
94465
94536
  // package.json
94466
94537
  var package_default = {
94467
94538
  name: "@hasna/testers",
94468
- version: "0.0.46",
94539
+ version: "0.0.48",
94469
94540
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
94470
94541
  type: "module",
94471
94542
  main: "dist/index.js",
@@ -96599,51 +96670,51 @@ function fromRow3(row) {
96599
96670
  };
96600
96671
  }
96601
96672
  function createEnvironment(input) {
96602
- const db2 = getDatabase();
96673
+ const db3 = getDatabase();
96603
96674
  const id = uuid();
96604
96675
  const timestamp = now();
96605
96676
  const meta = JSON.stringify({ variables: input.variables ?? {} });
96606
- db2.query(`
96677
+ db3.query(`
96607
96678
  INSERT INTO environments (id, name, url, auth_preset_name, project_id, is_default, metadata, created_at)
96608
96679
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
96609
96680
  `).run(id, input.name, input.url, input.authPresetName ?? null, input.projectId ?? null, input.isDefault ? 1 : 0, meta, timestamp);
96610
96681
  return getEnvironmentById(id);
96611
96682
  }
96612
96683
  function getEnvironmentById(id) {
96613
- const db2 = getDatabase();
96614
- const row = db2.query("SELECT * FROM environments WHERE id = ?").get(id);
96684
+ const db3 = getDatabase();
96685
+ const row = db3.query("SELECT * FROM environments WHERE id = ?").get(id);
96615
96686
  return row ? fromRow3(row) : null;
96616
96687
  }
96617
96688
  function getEnvironment(name) {
96618
- const db2 = getDatabase();
96619
- const row = db2.query("SELECT * FROM environments WHERE name = ?").get(name);
96689
+ const db3 = getDatabase();
96690
+ const row = db3.query("SELECT * FROM environments WHERE name = ?").get(name);
96620
96691
  return row ? fromRow3(row) : null;
96621
96692
  }
96622
96693
  function listEnvironments(projectId) {
96623
- const db2 = getDatabase();
96694
+ const db3 = getDatabase();
96624
96695
  if (projectId) {
96625
- const rows2 = db2.query("SELECT * FROM environments WHERE project_id = ? ORDER BY is_default DESC, created_at DESC").all(projectId);
96696
+ const rows2 = db3.query("SELECT * FROM environments WHERE project_id = ? ORDER BY is_default DESC, created_at DESC").all(projectId);
96626
96697
  return rows2.map(fromRow3);
96627
96698
  }
96628
- const rows = db2.query("SELECT * FROM environments ORDER BY is_default DESC, created_at DESC").all();
96699
+ const rows = db3.query("SELECT * FROM environments ORDER BY is_default DESC, created_at DESC").all();
96629
96700
  return rows.map(fromRow3);
96630
96701
  }
96631
96702
  function deleteEnvironment(name) {
96632
- const db2 = getDatabase();
96633
- const result = db2.query("DELETE FROM environments WHERE name = ?").run(name);
96703
+ const db3 = getDatabase();
96704
+ const result = db3.query("DELETE FROM environments WHERE name = ?").run(name);
96634
96705
  return result.changes > 0;
96635
96706
  }
96636
96707
  function setDefaultEnvironment(name) {
96637
- const db2 = getDatabase();
96638
- db2.exec("UPDATE environments SET is_default = 0");
96639
- const result = db2.query("UPDATE environments SET is_default = 1 WHERE name = ?").run(name);
96708
+ const db3 = getDatabase();
96709
+ db3.exec("UPDATE environments SET is_default = 0");
96710
+ const result = db3.query("UPDATE environments SET is_default = 1 WHERE name = ?").run(name);
96640
96711
  if (result.changes === 0) {
96641
96712
  throw new Error(`Environment not found: ${name}`);
96642
96713
  }
96643
96714
  }
96644
96715
  function getDefaultEnvironment() {
96645
- const db2 = getDatabase();
96646
- const row = db2.query("SELECT * FROM environments WHERE is_default = 1 LIMIT 1").get();
96716
+ const db3 = getDatabase();
96717
+ const row = db3.query("SELECT * FROM environments WHERE is_default = 1 LIMIT 1").get();
96647
96718
  return row ? fromRow3(row) : null;
96648
96719
  }
96649
96720
 
@@ -97210,12 +97281,12 @@ async function runRepoTests(opts) {
97210
97281
  specResults.push(result);
97211
97282
  const resultId = uuid();
97212
97283
  const timestamp = now();
97213
- const db2 = getDatabase();
97214
- db2.exec("PRAGMA foreign_keys = OFF");
97284
+ const db3 = getDatabase();
97285
+ db3.exec("PRAGMA foreign_keys = OFF");
97215
97286
  try {
97216
97287
  const reasoning = result.status === "passed" ? "All tests passed" : (result.error ?? "").slice(0, 500) || null;
97217
97288
  const errorStr = result.status !== "passed" ? result.error ?? null : null;
97218
- db2.query(`
97289
+ db3.query(`
97219
97290
  INSERT INTO results (id, run_id, scenario_id, status, reasoning, error, steps_completed, steps_total, duration_ms, model, tokens_used, cost_cents, metadata, created_at, persona_id, persona_name)
97220
97291
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, NULL, NULL)
97221
97292
  `).run(resultId, run.id, "__repo__", result.status, reasoning, errorStr, result.testResults.filter((t) => t.status === "passed").length, result.testResults.length || 1, result.durationMs, "repo-native", JSON.stringify({
@@ -97224,7 +97295,7 @@ async function runRepoTests(opts) {
97224
97295
  testResults: result.testResults
97225
97296
  }), timestamp);
97226
97297
  } finally {
97227
- db2.exec("PRAGMA foreign_keys = ON");
97298
+ db3.exec("PRAGMA foreign_keys = ON");
97228
97299
  }
97229
97300
  const resultRecord = { id: resultId };
97230
97301
  if (result.stdout || result.stderr) {
@@ -97365,6 +97436,39 @@ function validateStoredAssertion(value) {
97365
97436
  }
97366
97437
  return describeStoredAssertion(value);
97367
97438
  }
97439
+ function envCredentialRef(value) {
97440
+ const trimmed = value?.trim();
97441
+ if (!trimmed)
97442
+ return;
97443
+ return trimmed.startsWith("$") ? trimmed : `$${trimmed}`;
97444
+ }
97445
+ function assertEnvVarName(key, rawValue) {
97446
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
97447
+ throw new Error(`Invalid sandbox env var name: ${key || rawValue}`);
97448
+ }
97449
+ }
97450
+ function parseSandboxEnv(values, optionalValues = []) {
97451
+ if (!values?.length && !optionalValues?.length)
97452
+ return;
97453
+ const env = {};
97454
+ for (const value of values ?? []) {
97455
+ const trimmed = value.trim();
97456
+ if (!trimmed)
97457
+ continue;
97458
+ const separator = trimmed.indexOf("=");
97459
+ const key = separator >= 0 ? trimmed.slice(0, separator).trim() : trimmed;
97460
+ assertEnvVarName(key, value);
97461
+ env[key] = separator >= 0 ? trimmed.slice(separator + 1) : `$${key}`;
97462
+ }
97463
+ for (const value of optionalValues ?? []) {
97464
+ const key = value.trim();
97465
+ if (!key)
97466
+ continue;
97467
+ assertEnvVarName(key, value);
97468
+ env[key] = `$?${key}`;
97469
+ }
97470
+ return Object.keys(env).length > 0 ? env : undefined;
97471
+ }
97368
97472
  function AddForm({ onComplete }) {
97369
97473
  const { exit } = useApp();
97370
97474
  const [state, setState] = useState({
@@ -97669,12 +97773,12 @@ program2.command("add [name]").alias("create").description("Create a new test sc
97669
97773
  }, []).option("-t, --tag <tag>", "Tag (repeatable)", (val, acc) => {
97670
97774
  acc.push(val);
97671
97775
  return acc;
97672
- }, []).option("-p, --priority <level>", "Priority level", "medium").option("-m, --model <model>", "AI model to use").option("--path <path>", "Target path on the URL").option("--auth", "Requires authentication", false).option("--timeout <ms>", "Timeout in milliseconds").option("--project <id>", "Project ID").option("--template <name>", "Seed scenarios from a template (auth, crud, forms, nav, a11y)").option("--assert <assertion>", "Structured assertion (repeatable). Formats: selector:<sel> visible, text:<sel> contains:<text>, no-console-errors, url:contains:<path>, title:contains:<text>, count:<sel> eq:<n>", (val, acc) => {
97776
+ }, []).option("-p, --priority <level>", "Priority level", "medium").option("-m, --model <model>", "AI model to use").option("--path <path>", "Target path on the URL").option("--auth", "Requires authentication", false).option("--auth-preset <name>", "Attach email/password/loginPath from a named auth preset").option("--timeout <ms>", "Timeout in milliseconds").option("--project <id>", "Project ID").option("--template <name>", "Seed scenarios from a template (auth, crud, forms, nav, a11y)").option("--assert <assertion>", "Structured assertion (repeatable). Formats: selector:<sel> visible, text:<sel> contains:<text>, no-console-errors, url:contains:<path>, title:contains:<text>, count:<sel> eq:<n>", (val, acc) => {
97673
97777
  acc.push(val);
97674
97778
  return acc;
97675
97779
  }, []).action(async (name21, opts) => {
97676
97780
  try {
97677
- const hasFlags = opts.description || opts.steps?.length || opts.tag?.length || opts.model || opts.path || opts.auth || opts.timeout || opts.template || opts.assert?.length;
97781
+ const hasFlags = opts.description || opts.steps?.length || opts.tag?.length || opts.model || opts.path || opts.auth || opts.authPreset || opts.timeout || opts.template || opts.assert?.length;
97678
97782
  if (!name21 && !hasFlags) {
97679
97783
  const projectId2 = resolveProject2(opts.project);
97680
97784
  await runInteractiveAdd(projectId2);
@@ -97699,6 +97803,11 @@ program2.command("add [name]").alias("create").description("Create a new test sc
97699
97803
  }
97700
97804
  const assertions = opts.assert.map(parseAssertionString);
97701
97805
  const projectId = resolveProject2(opts.project);
97806
+ const authPreset = opts.authPreset ? getAuthPreset(opts.authPreset) : null;
97807
+ if (opts.authPreset && !authPreset) {
97808
+ logError(chalk6.red(`Auth preset not found: ${opts.authPreset}`));
97809
+ process.exit(1);
97810
+ }
97702
97811
  const scenario = createScenario({
97703
97812
  name: name21,
97704
97813
  description: opts.description || name21,
@@ -97707,7 +97816,12 @@ program2.command("add [name]").alias("create").description("Create a new test sc
97707
97816
  priority: opts.priority,
97708
97817
  model: opts.model,
97709
97818
  targetPath: opts.path,
97710
- requiresAuth: opts.auth,
97819
+ requiresAuth: opts.auth || Boolean(authPreset),
97820
+ authConfig: authPreset ? {
97821
+ email: authPreset.email,
97822
+ password: authPreset.password,
97823
+ loginPath: authPreset.loginPath
97824
+ } : undefined,
97711
97825
  timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
97712
97826
  assertions: assertions.length > 0 ? assertions : undefined,
97713
97827
  projectId
@@ -99701,12 +99815,22 @@ program2.command("report [run-id]").description("Generate HTML test report or co
99701
99815
  }
99702
99816
  });
99703
99817
  var authCmd = program2.command("auth").description("Manage auth presets");
99704
- authCmd.command("add <name>").description("Create an auth preset").requiredOption("--email <email>", "Login email").requiredOption("--password <password>", "Login password").option("--login-path <path>", "Login page path", "/login").action((name21, opts) => {
99818
+ authCmd.command("add <name>").description("Create an auth preset").option("--email <email>", "Login email or credential reference").option("--password <password>", "Login password or credential reference").option("--email-env <name>", "Environment variable name for the login email").option("--password-env <name>", "Environment variable name for the login password").option("--login-path <path>", "Login page path", "/login").action((name21, opts) => {
99705
99819
  try {
99820
+ const email3 = opts.email ?? envCredentialRef(opts.emailEnv);
99821
+ const password = opts.password ?? envCredentialRef(opts.passwordEnv);
99822
+ if (!email3) {
99823
+ logError(chalk6.red("Error: provide --email or --email-env"));
99824
+ process.exit(1);
99825
+ }
99826
+ if (!password) {
99827
+ logError(chalk6.red("Error: provide --password or --password-env"));
99828
+ process.exit(1);
99829
+ }
99706
99830
  const preset = createAuthPreset({
99707
99831
  name: name21,
99708
- email: opts.email,
99709
- password: opts.password,
99832
+ email: email3,
99833
+ password,
99710
99834
  loginPath: opts.loginPath
99711
99835
  });
99712
99836
  log(chalk6.green(`Created auth preset ${chalk6.bold(preset.name)} (${preset.email})`));
@@ -100501,8 +100625,8 @@ program2.command("doctor").description("Check system setup and configuration").a
100501
100625
  const dbPath = join20(getTestersDir(), "testers.db");
100502
100626
  try {
100503
100627
  const { Database: Database5 } = await import("bun:sqlite");
100504
- const db2 = new Database5(dbPath, { create: true });
100505
- db2.close();
100628
+ const db3 = new Database5(dbPath, { create: true });
100629
+ db3.close();
100506
100630
  log(chalk6.green("\u2713") + ` Database accessible: ${dbPath}`);
100507
100631
  } catch (err) {
100508
100632
  log(chalk6.red("\u2717") + ` Database not accessible at ${dbPath}: ${err instanceof Error ? err.message : String(err)}`);
@@ -100968,7 +101092,13 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
100968
101092
  }, []).option("--priority <level>", "Scenario priority").option("--persona <ids>", "Comma-separated persona IDs").option("--goal <prompt>", "Goal prompt for the agentic testing loop").option("--success <criteria>", "Success criteria (repeatable)", (val, acc) => {
100969
101093
  acc.push(val);
100970
101094
  return acc;
100971
- }, []).option("--max-iterations <n>", "Goal-loop iteration cap", "10").option("--target <target>", "Execution target: local or sandbox", "local").option("--sandbox-provider <provider>", "Sandbox provider: e2b, daytona, or modal").option("--sandbox-image <image>", "Sandbox image/template").option("--sandbox-remote-dir <path>", "Remote working directory for sandbox runs").option("--sandbox-cleanup <mode>", "Sandbox cleanup mode: delete, stop, or keep", "delete").option("--sandbox-sync <strategy>", "Sandbox upload sync strategy: rsync or archive", "rsync").option("--sandbox-setup-command <command>", "Shell command to run before testers in the sandbox").option("--sandbox-package <spec>", "Package spec to execute in the sandbox", "@hasna/testers").option("--sandbox-app-source <path>", "Local app source directory to upload into the sandbox").option("--sandbox-app-remote-dir <path>", "Remote app directory inside the sandbox (default: <sandbox-remote-dir>/app)").option("--sandbox-app-start-command <command>", "Shell command to start the app before testers runs").option("--sandbox-app-url <url>", "URL testers should target inside the sandbox after the app starts").option("--sandbox-app-wait-url <url>", "URL to poll before starting testers (defaults to --sandbox-app-url)").option("--sandbox-app-wait-timeout <ms>", "App readiness wait timeout in milliseconds").option("--e2b-template <name>", "Legacy alias for --sandbox-image").option("--timeout <ms>", "Workflow timeout").option("--json", "Output as JSON", false).action((name21, opts) => {
101095
+ }, []).option("--max-iterations <n>", "Goal-loop iteration cap", "10").option("--target <target>", "Execution target: local or sandbox", "local").option("--sandbox-provider <provider>", "Sandbox provider: e2b, daytona, or modal").option("--sandbox-image <image>", "Sandbox image/template").option("--sandbox-remote-dir <path>", "Remote working directory for sandbox runs").option("--sandbox-cleanup <mode>", "Sandbox cleanup mode: delete, stop, or keep", "delete").option("--sandbox-sync <strategy>", "Sandbox upload sync strategy: rsync or archive", "rsync").option("--sandbox-setup-command <command>", "Shell command to run before testers in the sandbox").option("--sandbox-package <spec>", "Package spec to execute in the sandbox", "@hasna/testers").option("--sandbox-env <assignment>", "Sandbox env var; KEY forwards host KEY, KEY=value stores value (repeatable)", (val, acc) => {
101096
+ acc.push(val);
101097
+ return acc;
101098
+ }, []).option("--sandbox-env-optional <name>", "Optional sandbox env var; forwards host NAME only when set (repeatable)", (val, acc) => {
101099
+ acc.push(val);
101100
+ return acc;
101101
+ }, []).option("--sandbox-app-source <path>", "Local app source directory to upload into the sandbox").option("--sandbox-app-remote-dir <path>", "Remote app directory inside the sandbox (default: <sandbox-remote-dir>/app)").option("--sandbox-app-start-command <command>", "Shell command to start the app before testers runs").option("--sandbox-app-url <url>", "URL testers should target inside the sandbox after the app starts").option("--sandbox-app-wait-url <url>", "URL to poll before starting testers (defaults to --sandbox-app-url)").option("--sandbox-app-wait-timeout <ms>", "App readiness wait timeout in milliseconds").option("--e2b-template <name>", "Legacy alias for --sandbox-image").option("--timeout <ms>", "Workflow timeout").option("--json", "Output as JSON", false).action((name21, opts) => {
100972
101102
  try {
100973
101103
  const workflow = createTestingWorkflow({
100974
101104
  name: name21,
@@ -100994,6 +101124,7 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
100994
101124
  sandboxSyncStrategy: opts.sandboxSync,
100995
101125
  setupCommand: opts.sandboxSetupCommand,
100996
101126
  packageSpec: opts.sandboxPackage,
101127
+ env: parseSandboxEnv(opts.sandboxEnv, opts.sandboxEnvOptional),
100997
101128
  appSourceDir: opts.sandboxAppSource,
100998
101129
  appRemoteDir: opts.sandboxAppRemoteDir,
100999
101130
  appStartCommand: opts.sandboxAppStartCommand,
@@ -101012,6 +101143,36 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
101012
101143
  process.exit(1);
101013
101144
  }
101014
101145
  });
101146
+ workflowCmd.command("update <id>").description("Update a saved testing workflow").option("--sandbox-env <assignment>", "Sandbox env var; KEY forwards host KEY, KEY=value stores value (repeatable)", (val, acc) => {
101147
+ acc.push(val);
101148
+ return acc;
101149
+ }, []).option("--sandbox-env-optional <name>", "Optional sandbox env var; forwards host NAME only when set (repeatable)", (val, acc) => {
101150
+ acc.push(val);
101151
+ return acc;
101152
+ }, []).option("--clear-sandbox-env", "Replace existing sandbox env vars instead of merging", false).option("--json", "Output as JSON", false).action((id, opts) => {
101153
+ try {
101154
+ const workflow = getTestingWorkflow(id);
101155
+ if (!workflow) {
101156
+ logError(chalk6.red(`Workflow not found: ${id}`));
101157
+ process.exit(1);
101158
+ }
101159
+ const envPatch = parseSandboxEnv(opts.sandboxEnv, opts.sandboxEnvOptional);
101160
+ const execution = {
101161
+ ...workflow.execution,
101162
+ env: opts.clearSandboxEnv ? envPatch : { ...workflow.execution.env ?? {}, ...envPatch ?? {} }
101163
+ };
101164
+ if (Object.keys(execution.env ?? {}).length === 0)
101165
+ delete execution.env;
101166
+ const updated = updateTestingWorkflow(workflow.id, { execution });
101167
+ if (opts.json)
101168
+ log(JSON.stringify(updated, null, 2));
101169
+ else
101170
+ log(chalk6.green(`Workflow updated: ${chalk6.bold(updated.id.slice(0, 8))} \u2014 ${updated.name}`));
101171
+ } catch (error40) {
101172
+ logError(chalk6.red(`Error: ${error40 instanceof Error ? error40.message : String(error40)}`));
101173
+ process.exit(1);
101174
+ }
101175
+ });
101015
101176
  workflowCmd.command("list").description("List saved testing workflows").option("--project <id>", "Project ID").option("--all", "Include disabled workflows", false).option("--json", "Output as JSON", false).action((opts) => {
101016
101177
  const workflows = listTestingWorkflows({
101017
101178
  projectId: opts.project ? resolveProject2(opts.project) : undefined,
package/dist/index.js CHANGED
@@ -17223,6 +17223,8 @@ var APP_SOURCE_EXCLUDES = [
17223
17223
  ".next",
17224
17224
  ".turbo",
17225
17225
  ".cache",
17226
+ ".env",
17227
+ ".env.*",
17226
17228
  ".venv",
17227
17229
  "__pycache__"
17228
17230
  ];
@@ -17445,7 +17447,7 @@ async function runViaSandbox(plan, dependencies) {
17445
17447
  workflowId: plan.workflow.id,
17446
17448
  workflowName: plan.workflow.name
17447
17449
  },
17448
- sandboxEnvVars: plan.sandbox.env,
17450
+ sandboxEnvVars: resolveSandboxEnv(plan.sandbox.env),
17449
17451
  cleanup: plan.sandbox.cleanup,
17450
17452
  upload: {
17451
17453
  localDir: bundle.localDir,
@@ -17471,6 +17473,26 @@ async function runViaSandbox(plan, dependencies) {
17471
17473
  bundle.cleanup?.();
17472
17474
  }
17473
17475
  }
17476
+ function resolveSandboxEnv(env2) {
17477
+ if (!env2 || Object.keys(env2).length === 0)
17478
+ return;
17479
+ const resolved = {};
17480
+ for (const [key, value] of Object.entries(env2)) {
17481
+ if (value.startsWith("$?")) {
17482
+ const optionalName = value.slice(2).trim();
17483
+ const optionalValue = optionalName ? process.env[optionalName] : undefined;
17484
+ if (optionalValue !== undefined)
17485
+ resolved[key] = optionalValue;
17486
+ continue;
17487
+ }
17488
+ const resolvedValue = resolveCredential(value);
17489
+ if (resolvedValue === null) {
17490
+ throw new Error(`Missing sandbox env value for ${key}`);
17491
+ }
17492
+ resolved[key] = resolvedValue;
17493
+ }
17494
+ return resolved;
17495
+ }
17474
17496
  async function resolveSandboxesRuntime(dependencies) {
17475
17497
  if (dependencies.sandboxes)
17476
17498
  return dependencies.sandboxes;
@@ -1 +1 @@
1
- {"version":3,"file":"workflow-runner.d.ts","sourceRoot":"","sources":["../../src/lib/workflow-runner.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"workflow-runner.d.ts","sourceRoot":"","sources":["../../src/lib/workflow-runner.ts"],"names":[],"mappings":"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"}
package/dist/mcp/index.js CHANGED
@@ -52,7 +52,7 @@ var package_default;
52
52
  var init_package = __esm(() => {
53
53
  package_default = {
54
54
  name: "@hasna/testers",
55
- version: "0.0.46",
55
+ version: "0.0.48",
56
56
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
57
57
  type: "module",
58
58
  main: "dist/index.js",
@@ -23723,7 +23723,7 @@ async function runViaSandbox(plan, dependencies) {
23723
23723
  workflowId: plan.workflow.id,
23724
23724
  workflowName: plan.workflow.name
23725
23725
  },
23726
- sandboxEnvVars: plan.sandbox.env,
23726
+ sandboxEnvVars: resolveSandboxEnv(plan.sandbox.env),
23727
23727
  cleanup: plan.sandbox.cleanup,
23728
23728
  upload: {
23729
23729
  localDir: bundle.localDir,
@@ -23749,6 +23749,26 @@ async function runViaSandbox(plan, dependencies) {
23749
23749
  bundle.cleanup?.();
23750
23750
  }
23751
23751
  }
23752
+ function resolveSandboxEnv(env2) {
23753
+ if (!env2 || Object.keys(env2).length === 0)
23754
+ return;
23755
+ const resolved = {};
23756
+ for (const [key, value] of Object.entries(env2)) {
23757
+ if (value.startsWith("$?")) {
23758
+ const optionalName = value.slice(2).trim();
23759
+ const optionalValue = optionalName ? process.env[optionalName] : undefined;
23760
+ if (optionalValue !== undefined)
23761
+ resolved[key] = optionalValue;
23762
+ continue;
23763
+ }
23764
+ const resolvedValue = resolveCredential(value);
23765
+ if (resolvedValue === null) {
23766
+ throw new Error(`Missing sandbox env value for ${key}`);
23767
+ }
23768
+ resolved[key] = resolvedValue;
23769
+ }
23770
+ return resolved;
23771
+ }
23752
23772
  async function resolveSandboxesRuntime(dependencies) {
23753
23773
  if (dependencies.sandboxes)
23754
23774
  return dependencies.sandboxes;
@@ -23766,6 +23786,7 @@ var init_workflow_runner = __esm(() => {
23766
23786
  init_workflows();
23767
23787
  init_personas();
23768
23788
  init_runner();
23789
+ init_secrets_resolver();
23769
23790
  APP_SOURCE_EXCLUDES = [
23770
23791
  "node_modules",
23771
23792
  ".git",
@@ -23773,6 +23794,8 @@ var init_workflow_runner = __esm(() => {
23773
23794
  ".next",
23774
23795
  ".turbo",
23775
23796
  ".cache",
23797
+ ".env",
23798
+ ".env.*",
23776
23799
  ".venv",
23777
23800
  "__pycache__"
23778
23801
  ];
@@ -46937,7 +46937,7 @@ import { join as join14 } from "path";
46937
46937
  // package.json
46938
46938
  var package_default = {
46939
46939
  name: "@hasna/testers",
46940
- version: "0.0.46",
46940
+ version: "0.0.48",
46941
46941
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
46942
46942
  type: "module",
46943
46943
  main: "dist/index.js",
@@ -51339,6 +51339,8 @@ var APP_SOURCE_EXCLUDES = [
51339
51339
  ".next",
51340
51340
  ".turbo",
51341
51341
  ".cache",
51342
+ ".env",
51343
+ ".env.*",
51342
51344
  ".venv",
51343
51345
  "__pycache__"
51344
51346
  ];
@@ -51561,7 +51563,7 @@ async function runViaSandbox(plan, dependencies) {
51561
51563
  workflowId: plan.workflow.id,
51562
51564
  workflowName: plan.workflow.name
51563
51565
  },
51564
- sandboxEnvVars: plan.sandbox.env,
51566
+ sandboxEnvVars: resolveSandboxEnv(plan.sandbox.env),
51565
51567
  cleanup: plan.sandbox.cleanup,
51566
51568
  upload: {
51567
51569
  localDir: bundle.localDir,
@@ -51587,6 +51589,26 @@ async function runViaSandbox(plan, dependencies) {
51587
51589
  bundle.cleanup?.();
51588
51590
  }
51589
51591
  }
51592
+ function resolveSandboxEnv(env) {
51593
+ if (!env || Object.keys(env).length === 0)
51594
+ return;
51595
+ const resolved = {};
51596
+ for (const [key, value] of Object.entries(env)) {
51597
+ if (value.startsWith("$?")) {
51598
+ const optionalName = value.slice(2).trim();
51599
+ const optionalValue = optionalName ? process.env[optionalName] : undefined;
51600
+ if (optionalValue !== undefined)
51601
+ resolved[key] = optionalValue;
51602
+ continue;
51603
+ }
51604
+ const resolvedValue = resolveCredential(value);
51605
+ if (resolvedValue === null) {
51606
+ throw new Error(`Missing sandbox env value for ${key}`);
51607
+ }
51608
+ resolved[key] = resolvedValue;
51609
+ }
51610
+ return resolved;
51611
+ }
51590
51612
  async function resolveSandboxesRuntime(dependencies) {
51591
51613
  if (dependencies.sandboxes)
51592
51614
  return dependencies.sandboxes;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/testers",
3
- "version": "0.0.46",
3
+ "version": "0.0.48",
4
4
  "description": "AI-powered QA testing CLI — spawns cheap AI agents to test web apps with headless browsers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",