@hasna/testers 0.0.37 → 0.0.39

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
@@ -16697,12 +16697,19 @@ var init_personas = __esm(() => {
16697
16697
  // src/lib/screenshotter.ts
16698
16698
  import { mkdirSync as mkdirSync7, existsSync as existsSync8, writeFileSync as writeFileSync2 } from "fs";
16699
16699
  import { join as join10 } from "path";
16700
- function slugify(text) {
16701
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
16700
+ function truncateSlug(slug, maxLength) {
16701
+ if (slug.length <= maxLength)
16702
+ return slug;
16703
+ const truncated = slug.slice(0, maxLength).replace(/-+$/g, "");
16704
+ return truncated || slug.slice(0, maxLength);
16705
+ }
16706
+ function slugify(text, maxLength) {
16707
+ const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
16708
+ return maxLength ? truncateSlug(slug, maxLength) : slug;
16702
16709
  }
16703
16710
  function generateFilename(stepNumber, action) {
16704
16711
  const padded = String(stepNumber).padStart(3, "0");
16705
- const slug = slugify(action);
16712
+ const slug = slugify(action, MAX_ACTION_SLUG_LENGTH);
16706
16713
  return `${padded}_${slug}.png`;
16707
16714
  }
16708
16715
  function formatDate(date) {
@@ -16716,7 +16723,8 @@ function getScreenshotDir(baseDir, runId, scenarioSlug, projectName, timestamp)
16716
16723
  const project = projectName ?? "default";
16717
16724
  const dateDir = formatDate(now2);
16718
16725
  const timeDir = `${formatTime(now2)}_${runId.slice(0, 8)}`;
16719
- return join10(baseDir, project, dateDir, timeDir, scenarioSlug);
16726
+ const safeScenarioSlug = slugify(scenarioSlug, MAX_SCENARIO_SLUG_LENGTH) || "scenario";
16727
+ return join10(baseDir, project, dateDir, timeDir, safeScenarioSlug);
16720
16728
  }
16721
16729
  function ensureDir(dirPath) {
16722
16730
  if (!existsSync8(dirPath)) {
@@ -16874,7 +16882,7 @@ class Screenshotter {
16874
16882
  };
16875
16883
  }
16876
16884
  }
16877
- var DEFAULT_BASE_DIR;
16885
+ var MAX_ACTION_SLUG_LENGTH = 80, MAX_SCENARIO_SLUG_LENGTH = 96, DEFAULT_BASE_DIR;
16878
16886
  var init_screenshotter = __esm(() => {
16879
16887
  init_paths();
16880
16888
  DEFAULT_BASE_DIR = join10(getTestersDir(), "screenshots");
@@ -18137,6 +18145,7 @@ __export(exports_runner, {
18137
18145
  runByFilter: () => runByFilter,
18138
18146
  runBatch: () => runBatch,
18139
18147
  resolveScenariosForRun: () => resolveScenariosForRun,
18148
+ resolveAgentMaxTurns: () => resolveAgentMaxTurns,
18140
18149
  resolveAgentApiKeyForModel: () => resolveAgentApiKeyForModel,
18141
18150
  onRunEvent: () => onRunEvent,
18142
18151
  applyStructuredAssertionsToResult: () => applyStructuredAssertionsToResult
@@ -18154,6 +18163,14 @@ function emit(event) {
18154
18163
  function resolveAgentApiKeyForModel(model, explicitApiKey, configuredAnthropicApiKey) {
18155
18164
  return resolveProviderApiKeyForModel(model, explicitApiKey, configuredAnthropicApiKey);
18156
18165
  }
18166
+ function resolveAgentMaxTurns(options) {
18167
+ if (options.maxTurns !== undefined) {
18168
+ const parsed = Math.floor(options.maxTurns);
18169
+ if (Number.isFinite(parsed) && parsed > 0)
18170
+ return parsed;
18171
+ }
18172
+ return options.minimal ? 10 : 30;
18173
+ }
18157
18174
  function assertionDescription(result) {
18158
18175
  return result.assertion.description || `${result.assertion.type}${result.assertion.selector ? ` ${result.assertion.selector}` : ""}`;
18159
18176
  }
@@ -18373,7 +18390,7 @@ async function runSingleScenario(scenario, runId, options) {
18373
18390
  runId,
18374
18391
  sessionId: result.id,
18375
18392
  baseUrl: options.url,
18376
- maxTurns: effectiveOptions.minimal ? 10 : 30,
18393
+ maxTurns: resolveAgentMaxTurns(effectiveOptions),
18377
18394
  a11y: effectiveOptions.a11y,
18378
18395
  persona: persona ? {
18379
18396
  name: persona.name,
@@ -93980,7 +93997,7 @@ import chalk6 from "chalk";
93980
93997
  // package.json
93981
93998
  var package_default = {
93982
93999
  name: "@hasna/testers",
93983
- version: "0.0.37",
94000
+ version: "0.0.39",
93984
94001
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
93985
94002
  type: "module",
93986
94003
  main: "dist/index.js",
@@ -94558,6 +94575,7 @@ function formatSmokeReport(result) {
94558
94575
 
94559
94576
  // src/lib/quick-qa.ts
94560
94577
  init_health_scan();
94578
+ var DEFAULT_QUICK_QA_OVERALL_TIMEOUT_MS = 120000;
94561
94579
  var DEFAULT_QUICK_QA_SCANNERS = [
94562
94580
  "console",
94563
94581
  "network",
@@ -94608,7 +94626,17 @@ function resolveQuickQaSelection(options = {}) {
94608
94626
  }
94609
94627
  async function runQuickQa(options) {
94610
94628
  const start = Date.now();
94611
- const health = await runHealthScan({
94629
+ const timeoutMs = options.overallTimeoutMs ?? DEFAULT_QUICK_QA_OVERALL_TIMEOUT_MS;
94630
+ return withQuickQaTimeout(runQuickQaUnbounded(options, start), {
94631
+ url: options.url,
94632
+ start,
94633
+ timeoutMs
94634
+ });
94635
+ }
94636
+ async function runQuickQaUnbounded(options, start) {
94637
+ const healthScanner = options.healthScanner ?? runHealthScan;
94638
+ const smokeRunner = options.smokeRunner ?? runSmoke;
94639
+ const health = await healthScanner({
94612
94640
  url: options.url,
94613
94641
  pages: options.pages,
94614
94642
  projectId: options.projectId,
@@ -94618,7 +94646,7 @@ async function runQuickQa(options) {
94618
94646
  maxPages: options.maxPages,
94619
94647
  wcagLevel: options.wcagLevel
94620
94648
  });
94621
- const smoke = options.includeSmoke === false ? null : await runSmoke({
94649
+ const smoke = options.includeSmoke === false ? null : await smokeRunner({
94622
94650
  url: options.url,
94623
94651
  model: options.model,
94624
94652
  headed: options.headed,
@@ -94632,6 +94660,62 @@ async function runQuickQa(options) {
94632
94660
  durationMs: Date.now() - start
94633
94661
  });
94634
94662
  }
94663
+ function withQuickQaTimeout(promise, options) {
94664
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)
94665
+ return promise;
94666
+ return new Promise((resolve, reject) => {
94667
+ const timer = setTimeout(() => {
94668
+ resolve(buildQuickQaTimeoutResult({
94669
+ url: options.url,
94670
+ start: options.start,
94671
+ timeoutMs: options.timeoutMs
94672
+ }));
94673
+ }, options.timeoutMs);
94674
+ promise.then((result) => {
94675
+ clearTimeout(timer);
94676
+ resolve(result);
94677
+ }, (error) => {
94678
+ clearTimeout(timer);
94679
+ reject(error);
94680
+ });
94681
+ });
94682
+ }
94683
+ function buildQuickQaTimeoutResult(input) {
94684
+ const now2 = new Date;
94685
+ const durationMs = Math.max(0, Date.now() - input.start);
94686
+ const message = `Quick QA timed out after ${input.timeoutMs}ms before all checks finished. Increase --overall-timeout or skip slow checks.`;
94687
+ const health = {
94688
+ url: input.url,
94689
+ scannedAt: now2.toISOString(),
94690
+ durationMs,
94691
+ totalIssues: 1,
94692
+ newIssues: 1,
94693
+ regressedIssues: 0,
94694
+ existingIssues: 0,
94695
+ results: [{
94696
+ url: input.url,
94697
+ pages: [input.url],
94698
+ scannedAt: now2.toISOString(),
94699
+ durationMs,
94700
+ issues: [{
94701
+ type: "performance",
94702
+ severity: "high",
94703
+ pageUrl: input.url,
94704
+ message,
94705
+ detail: {
94706
+ check: "quick-qa",
94707
+ timeoutMs: input.timeoutMs
94708
+ }
94709
+ }]
94710
+ }]
94711
+ };
94712
+ return buildQuickQaResult({
94713
+ url: input.url,
94714
+ health,
94715
+ smoke: null,
94716
+ durationMs
94717
+ });
94718
+ }
94635
94719
  function buildQuickQaResult(input) {
94636
94720
  const healthActionable = input.health.newIssues + input.health.regressedIssues;
94637
94721
  const smokeIssues = input.smoke?.issuesFound.length ?? 0;
@@ -97476,7 +97560,7 @@ program2.command("remove <id>").alias("uninstall").description("Remove a scenari
97476
97560
  program2.command("run [url] [description]").alias("test").description("Run test scenarios against a URL").option("-t, --tag <tag>", "Filter by tag (repeatable)", (val, acc) => {
97477
97561
  acc.push(val);
97478
97562
  return acc;
97479
- }, []).option("-s, --scenario <id>", "Run specific scenario ID").option("-p, --priority <level>", "Filter by priority").option("--headed", "Run browser in headed mode", false).option("-m, --model <model>", "AI model to use").option("--parallel <n>", "Number of parallel browsers", "1").option("--json", "Output results as JSON", false).option("-o, --output <filepath>", "Write JSON results to file").option("--timeout <ms>", "Timeout in milliseconds").option("--from-todos", "Import scenarios from todos before running", false).option("--project <id>", "Project ID").option("-b, --background", "Start run in background and return immediately", false).option("--browser <engine>", "Browser engine: playwright (default), lightpanda (9x faster, no screenshots), or bun (native WKWebView, 11x faster, Bun canary required)", "playwright").option("--env <name>", "Use a named environment for the URL").option("--dry-run", "Print what would run without launching browser", false).option("--retry <n>", "Retry failed scenarios up to n times", "0").option("--samples <n>", "Run each scenario N times and report flakiness (pass rate)", "1").option("--flakiness-threshold <n>", "Pass rate threshold below which a scenario is marked flaky (0-1)", "0.95").option("--a11y [level]", "Run axe-core WCAG accessibility scan after each navigation (level: A, AA, AAA \u2014 default AA)").option("--self-heal", "Enable AI-powered selector repair when elements can't be found (requires judgeModel or ANTHROPIC_API_KEY)", false).option("--verbose", "Show per-step timing and full tool results", false).option("--watch-results", "When used with --background, poll and display live results table until run completes", false).option("--failed-only", "Only show failed/error scenarios in output (passed count shown as summary)", false).option("--smoke", "Run only smoke-tagged scenarios (fast validation suite, <2 min)", false).option("--minimal", "Fastest possible run: cheapest model, max parallelism, min turns (ideal for CI)", false).option("--github-comment", "Post pass/fail summary as a GitHub PR comment (requires GITHUB_TOKEN env var)", false).option("--pr <number>", "GitHub PR number (auto-detected from GITHUB_REF if not provided)").option("--persona <id>", "Override persona for this run (comma-separated IDs for divergence testing)").option("--max-cost <dollars>", "Hard budget cap in dollars \u2014 abort if estimated cost exceeds this (e.g. 0.50 for 50 cents)").option("--cache-max-age <seconds>", "Skip scenarios that passed at the same URL within this many seconds (0 = disabled)", "0").option("--diff", "Auto-detect changed files from git diff and run only relevant scenarios", false).option("--auto-generate", "If no scenarios exist, crawl the URL and generate scenarios automatically (enabled by default when a URL is given as the first arg)").option("--no-auto-generate", "Disable automatic scenario generation when no scenarios exist").option("--overall-timeout <ms>", "Hard overall timeout for the whole run in milliseconds (default 10 minutes)").option("-y, --yes", "Skip confirmation prompts (e.g. proceed past budget warnings)", false).action(async (urlArg, description, opts) => {
97563
+ }, []).option("-s, --scenario <id>", "Run specific scenario ID").option("-p, --priority <level>", "Filter by priority").option("--headed", "Run browser in headed mode", false).option("-m, --model <model>", "AI model to use").option("--parallel <n>", "Number of parallel browsers", "1").option("--json", "Output results as JSON", false).option("-o, --output <filepath>", "Write JSON results to file").option("--timeout <ms>", "Timeout in milliseconds").option("--from-todos", "Import scenarios from todos before running", false).option("--project <id>", "Project ID").option("-b, --background", "Start run in background and return immediately", false).option("--browser <engine>", "Browser engine: playwright (default), lightpanda (9x faster, no screenshots), or bun (native WKWebView, 11x faster, Bun canary required)", "playwright").option("--env <name>", "Use a named environment for the URL").option("--dry-run", "Print what would run without launching browser", false).option("--retry <n>", "Retry failed scenarios up to n times", "0").option("--samples <n>", "Run each scenario N times and report flakiness (pass rate)", "1").option("--flakiness-threshold <n>", "Pass rate threshold below which a scenario is marked flaky (0-1)", "0.95").option("--a11y [level]", "Run axe-core WCAG accessibility scan after each navigation (level: A, AA, AAA \u2014 default AA)").option("--self-heal", "Enable AI-powered selector repair when elements can't be found (requires judgeModel or ANTHROPIC_API_KEY)", false).option("--verbose", "Show per-step timing and full tool results", false).option("--watch-results", "When used with --background, poll and display live results table until run completes", false).option("--failed-only", "Only show failed/error scenarios in output (passed count shown as summary)", false).option("--smoke", "Run only smoke-tagged scenarios (fast validation suite, <2 min)", false).option("--minimal", "Fastest possible run: cheapest model, max parallelism, min turns (ideal for CI)", false).option("--max-turns <n>", "Maximum AI browser-agent turns before reporting an error").option("--github-comment", "Post pass/fail summary as a GitHub PR comment (requires GITHUB_TOKEN env var)", false).option("--pr <number>", "GitHub PR number (auto-detected from GITHUB_REF if not provided)").option("--persona <id>", "Override persona for this run (comma-separated IDs for divergence testing)").option("--max-cost <dollars>", "Hard budget cap in dollars \u2014 abort if estimated cost exceeds this (e.g. 0.50 for 50 cents)").option("--cache-max-age <seconds>", "Skip scenarios that passed at the same URL within this many seconds (0 = disabled)", "0").option("--diff", "Auto-detect changed files from git diff and run only relevant scenarios", false).option("--auto-generate", "If no scenarios exist, crawl the URL and generate scenarios automatically (enabled by default when a URL is given as the first arg)").option("--no-auto-generate", "Disable automatic scenario generation when no scenarios exist").option("--overall-timeout <ms>", "Hard overall timeout for the whole run in milliseconds (default 10 minutes)").option("-y, --yes", "Skip confirmation prompts (e.g. proceed past budget warnings)", false).action(async (urlArg, description, opts) => {
97480
97564
  try {
97481
97565
  const projectId = resolveProject2(opts.project);
97482
97566
  let url2 = urlArg;
@@ -97626,6 +97710,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
97626
97710
  headed: opts.headed,
97627
97711
  parallel: parseInt(opts.parallel, 10),
97628
97712
  timeout: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
97713
+ maxTurns: opts.maxTurns ? parseInt(opts.maxTurns, 10) : undefined,
97629
97714
  projectId,
97630
97715
  engine: opts.browser
97631
97716
  });
@@ -97744,6 +97829,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
97744
97829
  headed: opts.headed,
97745
97830
  parallel: parseInt(opts.parallel, 10),
97746
97831
  timeout: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
97832
+ maxTurns: opts.maxTurns ? parseInt(opts.maxTurns, 10) : undefined,
97747
97833
  retry: parseInt(opts.retry ?? "0", 10),
97748
97834
  projectId,
97749
97835
  engine: opts.browser,
@@ -97848,6 +97934,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
97848
97934
  headed: opts.headed,
97849
97935
  parallel: parseInt(opts.parallel, 10),
97850
97936
  timeout: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
97937
+ maxTurns: opts.maxTurns ? parseInt(opts.maxTurns, 10) : undefined,
97851
97938
  retry: parseInt(opts.retry ?? "0", 10),
97852
97939
  projectId,
97853
97940
  engine: opts.browser,
@@ -99145,7 +99232,7 @@ program2.command("quick-qa <url>").alias("quick-check").description("Run a fast
99145
99232
  }, []).option("--max-pages <n>", "Max pages to crawl for link checks", "20").option("--skip <check>", "Skip a check: console|network|links|perf|smoke|a11y (repeatable)", (v2, acc) => {
99146
99233
  acc.push(v2);
99147
99234
  return acc;
99148
- }, []).option("--a11y [level]", "Include WCAG accessibility scan at A, AA, or AAA (default AA)").option("--no-smoke", "Skip autonomous smoke exploration").option("-m, --model <model>", "AI model for autonomous smoke", "quick").option("--headed", "Run browser checks in headed mode", false).option("--timeout <ms>", "Navigation timeout per page in ms", "15000").option("--project <id>", "Project ID for issue tracking").option("--json", "Output results as JSON", false).option("-o, --output <file>", "Write JSON results to a file").action(async (url2, opts) => {
99235
+ }, []).option("--a11y [level]", "Include WCAG accessibility scan at A, AA, or AAA (default AA)").option("--no-smoke", "Skip autonomous smoke exploration").option("-m, --model <model>", "AI model for autonomous smoke", "quick").option("--headed", "Run browser checks in headed mode", false).option("--timeout <ms>", "Navigation timeout per page in ms", "15000").option("--overall-timeout <ms>", "Hard overall timeout for the quick QA run in milliseconds", String(DEFAULT_QUICK_QA_OVERALL_TIMEOUT_MS)).option("--project <id>", "Project ID for issue tracking").option("--json", "Output results as JSON", false).option("-o, --output <file>", "Write JSON results to a file").action(async (url2, opts) => {
99149
99236
  try {
99150
99237
  const projectId = resolveProject2(opts.project);
99151
99238
  const includeA11y = opts.a11y !== undefined;
@@ -99166,6 +99253,7 @@ program2.command("quick-qa <url>").alias("quick-check").description("Run a fast
99166
99253
  projectId,
99167
99254
  headed: opts.headed,
99168
99255
  timeoutMs: parseInt(opts.timeout, 10),
99256
+ overallTimeoutMs: parseInt(opts.overallTimeout, 10),
99169
99257
  maxPages: parseInt(opts.maxPages, 10),
99170
99258
  scanners: selection.scanners,
99171
99259
  includeSmoke: selection.includeSmoke,
package/dist/index.js CHANGED
@@ -13880,12 +13880,21 @@ init_browser_lightpanda();
13880
13880
  init_paths();
13881
13881
  import { mkdirSync as mkdirSync7, existsSync as existsSync8, writeFileSync as writeFileSync2 } from "fs";
13882
13882
  import { join as join10 } from "path";
13883
- function slugify(text) {
13884
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
13883
+ var MAX_ACTION_SLUG_LENGTH = 80;
13884
+ var MAX_SCENARIO_SLUG_LENGTH = 96;
13885
+ function truncateSlug(slug, maxLength) {
13886
+ if (slug.length <= maxLength)
13887
+ return slug;
13888
+ const truncated = slug.slice(0, maxLength).replace(/-+$/g, "");
13889
+ return truncated || slug.slice(0, maxLength);
13890
+ }
13891
+ function slugify(text, maxLength) {
13892
+ const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
13893
+ return maxLength ? truncateSlug(slug, maxLength) : slug;
13885
13894
  }
13886
13895
  function generateFilename(stepNumber, action) {
13887
13896
  const padded = String(stepNumber).padStart(3, "0");
13888
- const slug = slugify(action);
13897
+ const slug = slugify(action, MAX_ACTION_SLUG_LENGTH);
13889
13898
  return `${padded}_${slug}.png`;
13890
13899
  }
13891
13900
  function formatDate(date) {
@@ -13899,7 +13908,8 @@ function getScreenshotDir(baseDir, runId, scenarioSlug, projectName, timestamp)
13899
13908
  const project = projectName ?? "default";
13900
13909
  const dateDir = formatDate(now2);
13901
13910
  const timeDir = `${formatTime(now2)}_${runId.slice(0, 8)}`;
13902
- return join10(baseDir, project, dateDir, timeDir, scenarioSlug);
13911
+ const safeScenarioSlug = slugify(scenarioSlug, MAX_SCENARIO_SLUG_LENGTH) || "scenario";
13912
+ return join10(baseDir, project, dateDir, timeDir, safeScenarioSlug);
13903
13913
  }
13904
13914
  function ensureDir(dirPath) {
13905
13915
  if (!existsSync8(dirPath)) {
@@ -16452,6 +16462,14 @@ function emit(event) {
16452
16462
  function resolveAgentApiKeyForModel(model, explicitApiKey, configuredAnthropicApiKey) {
16453
16463
  return resolveProviderApiKeyForModel(model, explicitApiKey, configuredAnthropicApiKey);
16454
16464
  }
16465
+ function resolveAgentMaxTurns(options) {
16466
+ if (options.maxTurns !== undefined) {
16467
+ const parsed = Math.floor(options.maxTurns);
16468
+ if (Number.isFinite(parsed) && parsed > 0)
16469
+ return parsed;
16470
+ }
16471
+ return options.minimal ? 10 : 30;
16472
+ }
16455
16473
  function assertionDescription(result) {
16456
16474
  return result.assertion.description || `${result.assertion.type}${result.assertion.selector ? ` ${result.assertion.selector}` : ""}`;
16457
16475
  }
@@ -16671,7 +16689,7 @@ async function runSingleScenario(scenario, runId, options) {
16671
16689
  runId,
16672
16690
  sessionId: result.id,
16673
16691
  baseUrl: options.url,
16674
- maxTurns: effectiveOptions.minimal ? 10 : 30,
16692
+ maxTurns: resolveAgentMaxTurns(effectiveOptions),
16675
16693
  a11y: effectiveOptions.a11y,
16676
16694
  persona: persona ? {
16677
16695
  name: persona.name,
@@ -19135,6 +19153,7 @@ async function notifyHealthScan(url, counts) {
19135
19153
  }
19136
19154
 
19137
19155
  // src/lib/quick-qa.ts
19156
+ var DEFAULT_QUICK_QA_OVERALL_TIMEOUT_MS = 120000;
19138
19157
  var DEFAULT_QUICK_QA_SCANNERS = [
19139
19158
  "console",
19140
19159
  "network",
@@ -19185,7 +19204,17 @@ function resolveQuickQaSelection(options = {}) {
19185
19204
  }
19186
19205
  async function runQuickQa(options) {
19187
19206
  const start = Date.now();
19188
- const health = await runHealthScan({
19207
+ const timeoutMs = options.overallTimeoutMs ?? DEFAULT_QUICK_QA_OVERALL_TIMEOUT_MS;
19208
+ return withQuickQaTimeout(runQuickQaUnbounded(options, start), {
19209
+ url: options.url,
19210
+ start,
19211
+ timeoutMs
19212
+ });
19213
+ }
19214
+ async function runQuickQaUnbounded(options, start) {
19215
+ const healthScanner = options.healthScanner ?? runHealthScan;
19216
+ const smokeRunner = options.smokeRunner ?? runSmoke;
19217
+ const health = await healthScanner({
19189
19218
  url: options.url,
19190
19219
  pages: options.pages,
19191
19220
  projectId: options.projectId,
@@ -19195,7 +19224,7 @@ async function runQuickQa(options) {
19195
19224
  maxPages: options.maxPages,
19196
19225
  wcagLevel: options.wcagLevel
19197
19226
  });
19198
- const smoke = options.includeSmoke === false ? null : await runSmoke({
19227
+ const smoke = options.includeSmoke === false ? null : await smokeRunner({
19199
19228
  url: options.url,
19200
19229
  model: options.model,
19201
19230
  headed: options.headed,
@@ -19209,6 +19238,62 @@ async function runQuickQa(options) {
19209
19238
  durationMs: Date.now() - start
19210
19239
  });
19211
19240
  }
19241
+ function withQuickQaTimeout(promise, options) {
19242
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)
19243
+ return promise;
19244
+ return new Promise((resolve, reject) => {
19245
+ const timer = setTimeout(() => {
19246
+ resolve(buildQuickQaTimeoutResult({
19247
+ url: options.url,
19248
+ start: options.start,
19249
+ timeoutMs: options.timeoutMs
19250
+ }));
19251
+ }, options.timeoutMs);
19252
+ promise.then((result) => {
19253
+ clearTimeout(timer);
19254
+ resolve(result);
19255
+ }, (error) => {
19256
+ clearTimeout(timer);
19257
+ reject(error);
19258
+ });
19259
+ });
19260
+ }
19261
+ function buildQuickQaTimeoutResult(input) {
19262
+ const now2 = new Date;
19263
+ const durationMs = Math.max(0, Date.now() - input.start);
19264
+ const message = `Quick QA timed out after ${input.timeoutMs}ms before all checks finished. Increase --overall-timeout or skip slow checks.`;
19265
+ const health = {
19266
+ url: input.url,
19267
+ scannedAt: now2.toISOString(),
19268
+ durationMs,
19269
+ totalIssues: 1,
19270
+ newIssues: 1,
19271
+ regressedIssues: 0,
19272
+ existingIssues: 0,
19273
+ results: [{
19274
+ url: input.url,
19275
+ pages: [input.url],
19276
+ scannedAt: now2.toISOString(),
19277
+ durationMs,
19278
+ issues: [{
19279
+ type: "performance",
19280
+ severity: "high",
19281
+ pageUrl: input.url,
19282
+ message,
19283
+ detail: {
19284
+ check: "quick-qa",
19285
+ timeoutMs: input.timeoutMs
19286
+ }
19287
+ }]
19288
+ }]
19289
+ };
19290
+ return buildQuickQaResult({
19291
+ url: input.url,
19292
+ health,
19293
+ smoke: null,
19294
+ durationMs
19295
+ });
19296
+ }
19212
19297
  function buildQuickQaResult(input) {
19213
19298
  const healthActionable = input.health.newIssues + input.health.regressedIssues;
19214
19299
  const smokeIssues = input.smoke?.issuesFound.length ?? 0;
@@ -1,8 +1,9 @@
1
- import { type HealthScanOptions, type HealthScanSummary } from "./health-scan.js";
2
- import { type SmokeResult } from "./smoke.js";
1
+ import { runHealthScan, type HealthScanOptions, type HealthScanSummary } from "./health-scan.js";
2
+ import { runSmoke, type SmokeResult } from "./smoke.js";
3
3
  export type QuickQaScanner = NonNullable<HealthScanOptions["scanners"]>[number];
4
4
  export type QuickQaSkipTarget = QuickQaScanner | "smoke";
5
5
  export type QuickQaStatus = "passed" | "warn" | "failed";
6
+ export declare const DEFAULT_QUICK_QA_OVERALL_TIMEOUT_MS = 120000;
6
7
  export declare const DEFAULT_QUICK_QA_SCANNERS: QuickQaScanner[];
7
8
  export interface QuickQaSelection {
8
9
  scanners: QuickQaScanner[];
@@ -15,11 +16,14 @@ export interface QuickQaOptions {
15
16
  projectId?: string;
16
17
  headed?: boolean;
17
18
  timeoutMs?: number;
19
+ overallTimeoutMs?: number;
18
20
  maxPages?: number;
19
21
  scanners?: QuickQaScanner[];
20
22
  includeSmoke?: boolean;
21
23
  model?: string;
22
24
  wcagLevel?: "A" | "AA" | "AAA";
25
+ healthScanner?: typeof runHealthScan;
26
+ smokeRunner?: typeof runSmoke;
23
27
  }
24
28
  export interface QuickQaCheckSummary {
25
29
  name: "health" | "smoke";
@@ -50,6 +54,11 @@ export declare function resolveQuickQaSelection(options?: {
50
54
  scanners?: QuickQaScanner[];
51
55
  }): QuickQaSelection;
52
56
  export declare function runQuickQa(options: QuickQaOptions): Promise<QuickQaResult>;
57
+ export declare function buildQuickQaTimeoutResult(input: {
58
+ url: string;
59
+ start: number;
60
+ timeoutMs: number;
61
+ }): QuickQaResult;
53
62
  export declare function buildQuickQaResult(input: {
54
63
  url: string;
55
64
  health: HealthScanSummary;
@@ -1 +1 @@
1
- {"version":3,"file":"quick-qa.d.ts","sourceRoot":"","sources":["../../src/lib/quick-qa.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACjG,OAAO,EAAY,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAExD,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAChF,MAAM,MAAM,iBAAiB,GAAG,cAAc,GAAG,OAAO,CAAC;AACzD,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEzD,eAAO,MAAM,yBAAyB,EAAE,cAAc,EAKrD,CAAC;AAgBF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,OAAO,EAAE,iBAAiB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;IAC5B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,GAAG,GAAG,IAAI,GAAG,KAAK,CAAC;CAChC;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,MAAM,EAAE,aAAa,GAAG,SAAS,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,aAAa,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,WAAW,EAAE;QACX,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,GAAG,GAAG,IAAI,GAAG,KAAK,CAK5E;AAED,wBAAgB,uBAAuB,CAAC,OAAO,GAAE;IAC/C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;CACxB,GAAG,gBAAgB,CAwBxB;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CA6BhF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,iBAAiB,CAAC;IAC1B,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,aAAa,CA2DhB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAEhE;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CA8CjE"}
1
+ {"version":3,"file":"quick-qa.d.ts","sourceRoot":"","sources":["../../src/lib/quick-qa.ts"],"names":[],"mappings":"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"}
@@ -23,6 +23,7 @@ export interface RunOptions {
23
23
  skipBudgetCheck?: boolean;
24
24
  cacheMaxAgeMs?: number;
25
25
  minimal?: boolean;
26
+ maxTurns?: number;
26
27
  recordVideo?: boolean;
27
28
  }
28
29
  export interface RunEvent {
@@ -47,6 +48,7 @@ export interface RunEvent {
47
48
  export type RunEventHandler = (event: RunEvent) => void;
48
49
  export declare function onRunEvent(handler: RunEventHandler): void;
49
50
  export declare function resolveAgentApiKeyForModel(model: string, explicitApiKey?: string, configuredAnthropicApiKey?: string): string | undefined;
51
+ export declare function resolveAgentMaxTurns(options: Pick<RunOptions, "minimal" | "maxTurns">): number;
50
52
  type AgentScenarioStatus = Extract<ResultStatus, "passed" | "failed" | "error">;
51
53
  export interface StructuredAssertionOutcome {
52
54
  status: AgentScenarioStatus;
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import type { Page } from "playwright";
2
- export declare function slugify(text: string): string;
2
+ export declare function slugify(text: string, maxLength?: number): string;
3
3
  export declare function generateFilename(stepNumber: number, action: string): string;
4
4
  /**
5
5
  * Build the screenshot directory for a run:
@@ -1 +1 @@
1
- {"version":3,"file":"screenshotter.d.ts","sourceRoot":"","sources":["../../src/lib/screenshotter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAOvC,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK5C;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAI3E;AAUD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,WAAW,CAAC,EAAE,MAAM,EACpB,SAAS,CAAC,EAAE,IAAI,GACf,MAAM,CAMR;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAI/C;AAID,UAAU,oBAAoB;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,cAAc;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAuBD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GAC5G,IAAI,CAON;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACxH,IAAI,CAON;AAkCD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,YAAY,CAAO;gBAEf,OAAO,GAAE,oBAAyB;IASxC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAmDpE,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAiD5E,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;CA6CpG"}
1
+ {"version":3,"file":"screenshotter.d.ts","sourceRoot":"","sources":["../../src/lib/screenshotter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAgBvC,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAI3E;AAUD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,WAAW,CAAC,EAAE,MAAM,EACpB,SAAS,CAAC,EAAE,IAAI,GACf,MAAM,CAOR;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAI/C;AAID,UAAU,oBAAoB;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,cAAc;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAuBD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GAC5G,IAAI,CAON;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACxH,IAAI,CAON;AAkCD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,YAAY,CAAO;gBAEf,OAAO,GAAE,oBAAyB;IASxC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAmDpE,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAiD5E,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;CA6CpG"}
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.37",
55
+ version: "0.0.39",
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",
@@ -19805,12 +19805,19 @@ var init_personas = __esm(() => {
19805
19805
  // src/lib/screenshotter.ts
19806
19806
  import { mkdirSync as mkdirSync7, existsSync as existsSync8, writeFileSync as writeFileSync2 } from "fs";
19807
19807
  import { join as join10 } from "path";
19808
- function slugify(text) {
19809
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
19808
+ function truncateSlug(slug, maxLength) {
19809
+ if (slug.length <= maxLength)
19810
+ return slug;
19811
+ const truncated = slug.slice(0, maxLength).replace(/-+$/g, "");
19812
+ return truncated || slug.slice(0, maxLength);
19813
+ }
19814
+ function slugify(text, maxLength) {
19815
+ const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
19816
+ return maxLength ? truncateSlug(slug, maxLength) : slug;
19810
19817
  }
19811
19818
  function generateFilename(stepNumber, action) {
19812
19819
  const padded = String(stepNumber).padStart(3, "0");
19813
- const slug = slugify(action);
19820
+ const slug = slugify(action, MAX_ACTION_SLUG_LENGTH);
19814
19821
  return `${padded}_${slug}.png`;
19815
19822
  }
19816
19823
  function formatDate(date) {
@@ -19824,7 +19831,8 @@ function getScreenshotDir(baseDir, runId, scenarioSlug, projectName, timestamp)
19824
19831
  const project = projectName ?? "default";
19825
19832
  const dateDir = formatDate(now2);
19826
19833
  const timeDir = `${formatTime(now2)}_${runId.slice(0, 8)}`;
19827
- return join10(baseDir, project, dateDir, timeDir, scenarioSlug);
19834
+ const safeScenarioSlug = slugify(scenarioSlug, MAX_SCENARIO_SLUG_LENGTH) || "scenario";
19835
+ return join10(baseDir, project, dateDir, timeDir, safeScenarioSlug);
19828
19836
  }
19829
19837
  function ensureDir(dirPath) {
19830
19838
  if (!existsSync8(dirPath)) {
@@ -19982,7 +19990,7 @@ class Screenshotter {
19982
19990
  };
19983
19991
  }
19984
19992
  }
19985
- var DEFAULT_BASE_DIR;
19993
+ var MAX_ACTION_SLUG_LENGTH = 80, MAX_SCENARIO_SLUG_LENGTH = 96, DEFAULT_BASE_DIR;
19986
19994
  var init_screenshotter = __esm(() => {
19987
19995
  init_paths();
19988
19996
  DEFAULT_BASE_DIR = join10(getTestersDir(), "screenshots");
@@ -21180,6 +21188,7 @@ __export(exports_runner, {
21180
21188
  runByFilter: () => runByFilter,
21181
21189
  runBatch: () => runBatch,
21182
21190
  resolveScenariosForRun: () => resolveScenariosForRun,
21191
+ resolveAgentMaxTurns: () => resolveAgentMaxTurns,
21183
21192
  resolveAgentApiKeyForModel: () => resolveAgentApiKeyForModel,
21184
21193
  onRunEvent: () => onRunEvent,
21185
21194
  applyStructuredAssertionsToResult: () => applyStructuredAssertionsToResult
@@ -21197,6 +21206,14 @@ function emit(event) {
21197
21206
  function resolveAgentApiKeyForModel(model, explicitApiKey, configuredAnthropicApiKey) {
21198
21207
  return resolveProviderApiKeyForModel(model, explicitApiKey, configuredAnthropicApiKey);
21199
21208
  }
21209
+ function resolveAgentMaxTurns(options) {
21210
+ if (options.maxTurns !== undefined) {
21211
+ const parsed = Math.floor(options.maxTurns);
21212
+ if (Number.isFinite(parsed) && parsed > 0)
21213
+ return parsed;
21214
+ }
21215
+ return options.minimal ? 10 : 30;
21216
+ }
21200
21217
  function assertionDescription(result) {
21201
21218
  return result.assertion.description || `${result.assertion.type}${result.assertion.selector ? ` ${result.assertion.selector}` : ""}`;
21202
21219
  }
@@ -21416,7 +21433,7 @@ async function runSingleScenario(scenario, runId, options) {
21416
21433
  runId,
21417
21434
  sessionId: result.id,
21418
21435
  baseUrl: options.url,
21419
- maxTurns: effectiveOptions.minimal ? 10 : 30,
21436
+ maxTurns: resolveAgentMaxTurns(effectiveOptions),
21420
21437
  a11y: effectiveOptions.a11y,
21421
21438
  persona: persona ? {
21422
21439
  name: persona.name,
@@ -46910,7 +46910,7 @@ import { join as join14 } from "path";
46910
46910
  // package.json
46911
46911
  var package_default = {
46912
46912
  name: "@hasna/testers",
46913
- version: "0.0.37",
46913
+ version: "0.0.39",
46914
46914
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
46915
46915
  type: "module",
46916
46916
  main: "dist/index.js",
@@ -48303,12 +48303,21 @@ init_browser();
48303
48303
  init_paths();
48304
48304
  import { mkdirSync as mkdirSync5, existsSync as existsSync7, writeFileSync as writeFileSync2 } from "fs";
48305
48305
  import { join as join9 } from "path";
48306
- function slugify(text) {
48307
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
48306
+ var MAX_ACTION_SLUG_LENGTH = 80;
48307
+ var MAX_SCENARIO_SLUG_LENGTH = 96;
48308
+ function truncateSlug(slug, maxLength) {
48309
+ if (slug.length <= maxLength)
48310
+ return slug;
48311
+ const truncated = slug.slice(0, maxLength).replace(/-+$/g, "");
48312
+ return truncated || slug.slice(0, maxLength);
48313
+ }
48314
+ function slugify(text, maxLength) {
48315
+ const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
48316
+ return maxLength ? truncateSlug(slug, maxLength) : slug;
48308
48317
  }
48309
48318
  function generateFilename(stepNumber, action) {
48310
48319
  const padded = String(stepNumber).padStart(3, "0");
48311
- const slug = slugify(action);
48320
+ const slug = slugify(action, MAX_ACTION_SLUG_LENGTH);
48312
48321
  return `${padded}_${slug}.png`;
48313
48322
  }
48314
48323
  function formatDate(date) {
@@ -48322,7 +48331,8 @@ function getScreenshotDir(baseDir, runId, scenarioSlug, projectName, timestamp)
48322
48331
  const project = projectName ?? "default";
48323
48332
  const dateDir = formatDate(now2);
48324
48333
  const timeDir = `${formatTime(now2)}_${runId.slice(0, 8)}`;
48325
- return join9(baseDir, project, dateDir, timeDir, scenarioSlug);
48334
+ const safeScenarioSlug = slugify(scenarioSlug, MAX_SCENARIO_SLUG_LENGTH) || "scenario";
48335
+ return join9(baseDir, project, dateDir, timeDir, safeScenarioSlug);
48326
48336
  }
48327
48337
  function ensureDir(dirPath) {
48328
48338
  if (!existsSync7(dirPath)) {
@@ -49404,6 +49414,14 @@ function emit(event) {
49404
49414
  function resolveAgentApiKeyForModel(model, explicitApiKey, configuredAnthropicApiKey) {
49405
49415
  return resolveProviderApiKeyForModel(model, explicitApiKey, configuredAnthropicApiKey);
49406
49416
  }
49417
+ function resolveAgentMaxTurns(options) {
49418
+ if (options.maxTurns !== undefined) {
49419
+ const parsed = Math.floor(options.maxTurns);
49420
+ if (Number.isFinite(parsed) && parsed > 0)
49421
+ return parsed;
49422
+ }
49423
+ return options.minimal ? 10 : 30;
49424
+ }
49407
49425
  function assertionDescription(result) {
49408
49426
  return result.assertion.description || `${result.assertion.type}${result.assertion.selector ? ` ${result.assertion.selector}` : ""}`;
49409
49427
  }
@@ -49623,7 +49641,7 @@ async function runSingleScenario(scenario, runId, options) {
49623
49641
  runId,
49624
49642
  sessionId: result.id,
49625
49643
  baseUrl: options.url,
49626
- maxTurns: effectiveOptions.minimal ? 10 : 30,
49644
+ maxTurns: resolveAgentMaxTurns(effectiveOptions),
49627
49645
  a11y: effectiveOptions.a11y,
49628
49646
  persona: persona ? {
49629
49647
  name: persona.name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/testers",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
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",