@hasna/testers 0.0.3 → 0.0.5

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/README.md CHANGED
@@ -228,7 +228,7 @@ Screenshots are saved to `~/.testers/screenshots/` organized by:
228
228
  Install for Claude Code:
229
229
 
230
230
  ```bash
231
- claude mcp add --transport stdio --scope user testers-mcp -- testers-mcp
231
+ claude mcp add --transport stdio --scope user testers -- testers-mcp
232
232
  ```
233
233
 
234
234
  Available tools: `create_scenario`, `list_scenarios`, `run_scenarios`, `get_results`, `get_screenshots`, and more.
package/dist/cli/index.js CHANGED
@@ -3641,7 +3641,8 @@ async function runAgentLoop(options) {
3641
3641
  screenshotter,
3642
3642
  model,
3643
3643
  runId,
3644
- maxTurns = 30
3644
+ maxTurns = 30,
3645
+ onStep
3645
3646
  } = options;
3646
3647
  const systemPrompt = [
3647
3648
  "You are an expert QA testing agent. Your job is to thoroughly test web application scenarios.",
@@ -3700,8 +3701,8 @@ async function runAgentLoop(options) {
3700
3701
  }
3701
3702
  const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
3702
3703
  if (toolUseBlocks.length === 0 && response.stop_reason === "end_turn") {
3703
- const textBlocks = response.content.filter((block) => block.type === "text");
3704
- const textReasoning = textBlocks.map((b) => b.text).join(`
3704
+ const textBlocks2 = response.content.filter((block) => block.type === "text");
3705
+ const textReasoning = textBlocks2.map((b) => b.text).join(`
3705
3706
  `);
3706
3707
  return {
3707
3708
  status: "error",
@@ -3712,10 +3713,22 @@ async function runAgentLoop(options) {
3712
3713
  };
3713
3714
  }
3714
3715
  const toolResults = [];
3716
+ const textBlocks = response.content.filter((block) => block.type === "text");
3717
+ if (textBlocks.length > 0 && onStep) {
3718
+ const thinking = textBlocks.map((b) => b.text).join(`
3719
+ `);
3720
+ onStep({ type: "thinking", thinking, stepNumber });
3721
+ }
3715
3722
  for (const toolBlock of toolUseBlocks) {
3716
3723
  stepNumber++;
3717
3724
  const toolInput = toolBlock.input;
3725
+ if (onStep) {
3726
+ onStep({ type: "tool_call", toolName: toolBlock.name, toolInput, stepNumber });
3727
+ }
3718
3728
  const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber });
3729
+ if (onStep) {
3730
+ onStep({ type: "tool_result", toolName: toolBlock.name, toolResult: execResult.result, stepNumber });
3731
+ }
3719
3732
  if (execResult.screenshot) {
3720
3733
  screenshots.push({
3721
3734
  ...execResult.screenshot,
@@ -3824,6 +3837,9 @@ function loadConfig() {
3824
3837
 
3825
3838
  // src/lib/runner.ts
3826
3839
  var eventHandler = null;
3840
+ function onRunEvent(handler) {
3841
+ eventHandler = handler;
3842
+ }
3827
3843
  function emit(event) {
3828
3844
  if (eventHandler)
3829
3845
  eventHandler(event);
@@ -3858,7 +3874,20 @@ async function runSingleScenario(scenario, runId, options) {
3858
3874
  screenshotter,
3859
3875
  model,
3860
3876
  runId,
3861
- maxTurns: 30
3877
+ maxTurns: 30,
3878
+ onStep: (stepEvent) => {
3879
+ emit({
3880
+ type: `step:${stepEvent.type}`,
3881
+ scenarioId: scenario.id,
3882
+ scenarioName: scenario.name,
3883
+ runId,
3884
+ toolName: stepEvent.toolName,
3885
+ toolInput: stepEvent.toolInput,
3886
+ toolResult: stepEvent.toolResult,
3887
+ thinking: stepEvent.thinking,
3888
+ stepNumber: stepEvent.stepNumber
3889
+ });
3890
+ }
3862
3891
  });
3863
3892
  for (const ss of agentResult.screenshots) {
3864
3893
  createScreenshot({
@@ -3968,6 +3997,79 @@ async function runByFilter(options) {
3968
3997
  }
3969
3998
  return runBatch(scenarios, options);
3970
3999
  }
4000
+ function startRunAsync(options) {
4001
+ const config = loadConfig();
4002
+ const model = resolveModel(options.model ?? config.defaultModel);
4003
+ let scenarios;
4004
+ if (options.scenarioIds && options.scenarioIds.length > 0) {
4005
+ const all = listScenarios({ projectId: options.projectId });
4006
+ scenarios = all.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
4007
+ } else {
4008
+ scenarios = listScenarios({
4009
+ projectId: options.projectId,
4010
+ tags: options.tags,
4011
+ priority: options.priority
4012
+ });
4013
+ }
4014
+ const parallel = options.parallel ?? 1;
4015
+ const run = createRun({
4016
+ url: options.url,
4017
+ model,
4018
+ headed: options.headed,
4019
+ parallel,
4020
+ projectId: options.projectId
4021
+ });
4022
+ if (scenarios.length === 0) {
4023
+ updateRun(run.id, { status: "passed", total: 0, finished_at: new Date().toISOString() });
4024
+ return { runId: run.id, scenarioCount: 0 };
4025
+ }
4026
+ updateRun(run.id, { status: "running", total: scenarios.length });
4027
+ (async () => {
4028
+ const results = [];
4029
+ try {
4030
+ if (parallel <= 1) {
4031
+ for (const scenario of scenarios) {
4032
+ const result = await runSingleScenario(scenario, run.id, options);
4033
+ results.push(result);
4034
+ }
4035
+ } else {
4036
+ const queue = [...scenarios];
4037
+ const running = [];
4038
+ const processNext = async () => {
4039
+ const scenario = queue.shift();
4040
+ if (!scenario)
4041
+ return;
4042
+ const result = await runSingleScenario(scenario, run.id, options);
4043
+ results.push(result);
4044
+ await processNext();
4045
+ };
4046
+ const workers = Math.min(parallel, scenarios.length);
4047
+ for (let i = 0;i < workers; i++) {
4048
+ running.push(processNext());
4049
+ }
4050
+ await Promise.all(running);
4051
+ }
4052
+ const passed = results.filter((r) => r.status === "passed").length;
4053
+ const failed = results.filter((r) => r.status === "failed" || r.status === "error").length;
4054
+ updateRun(run.id, {
4055
+ status: failed > 0 ? "failed" : "passed",
4056
+ passed,
4057
+ failed,
4058
+ total: scenarios.length,
4059
+ finished_at: new Date().toISOString()
4060
+ });
4061
+ emit({ type: "run:complete", runId: run.id });
4062
+ } catch (error) {
4063
+ const errorMsg = error instanceof Error ? error.message : String(error);
4064
+ updateRun(run.id, {
4065
+ status: "failed",
4066
+ finished_at: new Date().toISOString()
4067
+ });
4068
+ emit({ type: "run:complete", runId: run.id, error: errorMsg });
4069
+ }
4070
+ })();
4071
+ return { runId: run.id, scenarioCount: scenarios.length };
4072
+ }
3971
4073
  function estimateCost(model, tokens) {
3972
4074
  const costs = {
3973
4075
  "claude-haiku-4-5-20251001": 0.1,
@@ -5289,8 +5391,17 @@ function deleteAuthPreset(name) {
5289
5391
 
5290
5392
  // src/cli/index.tsx
5291
5393
  import { existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
5394
+ function formatToolInput(input) {
5395
+ const parts = [];
5396
+ for (const [key, value] of Object.entries(input)) {
5397
+ const str = typeof value === "string" ? value : JSON.stringify(value);
5398
+ const truncated = str.length > 60 ? str.slice(0, 60) + "..." : str;
5399
+ parts.push(`${key}="${truncated}"`);
5400
+ }
5401
+ return parts.join(" ");
5402
+ }
5292
5403
  var program2 = new Command;
5293
- program2.name("testers").version("0.0.1").description("AI-powered browser testing CLI");
5404
+ program2.name("testers").version("0.0.4").description("AI-powered browser testing CLI");
5294
5405
  var CONFIG_DIR2 = join6(process.env["HOME"] ?? "~", ".testers");
5295
5406
  var CONFIG_PATH2 = join6(CONFIG_DIR2, "config.json");
5296
5407
  function getActiveProject() {
@@ -5442,9 +5553,75 @@ program2.command("delete <id>").description("Delete a scenario").action((id) =>
5442
5553
  program2.command("run <url> [description]").description("Run test scenarios against a URL").option("-t, --tag <tag>", "Filter by tag (repeatable)", (val, acc) => {
5443
5554
  acc.push(val);
5444
5555
  return acc;
5445
- }, []).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").action(async (url, description, opts) => {
5556
+ }, []).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).action(async (url, description, opts) => {
5446
5557
  try {
5447
5558
  const projectId = resolveProject(opts.project);
5559
+ if (opts.fromTodos) {
5560
+ const result = importFromTodos({ projectId });
5561
+ console.log(chalk4.blue(`Imported ${result.imported} scenarios from todos (${result.skipped} skipped)`));
5562
+ }
5563
+ if (opts.background) {
5564
+ if (description) {
5565
+ createScenario({ name: description, description, tags: ["ad-hoc"], projectId });
5566
+ }
5567
+ const { runId, scenarioCount } = startRunAsync({
5568
+ url,
5569
+ tags: opts.tag.length > 0 ? opts.tag : undefined,
5570
+ scenarioIds: opts.scenario ? [opts.scenario] : undefined,
5571
+ priority: opts.priority,
5572
+ model: opts.model,
5573
+ headed: opts.headed,
5574
+ parallel: parseInt(opts.parallel, 10),
5575
+ timeout: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
5576
+ projectId
5577
+ });
5578
+ console.log(chalk4.green(`Run started in background: ${chalk4.bold(runId.slice(0, 8))}`));
5579
+ console.log(chalk4.dim(` Scenarios: ${scenarioCount}`));
5580
+ console.log(chalk4.dim(` URL: ${url}`));
5581
+ console.log(chalk4.dim(` Check progress: testers results ${runId.slice(0, 8)}`));
5582
+ process.exit(0);
5583
+ }
5584
+ if (!opts.json && !opts.output) {
5585
+ onRunEvent((event) => {
5586
+ switch (event.type) {
5587
+ case "scenario:start":
5588
+ console.log(chalk4.blue(` [start] ${event.scenarioName ?? event.scenarioId}`));
5589
+ break;
5590
+ case "step:thinking":
5591
+ if (event.thinking) {
5592
+ const preview = event.thinking.length > 120 ? event.thinking.slice(0, 120) + "..." : event.thinking;
5593
+ console.log(chalk4.dim(` [think] ${preview}`));
5594
+ }
5595
+ break;
5596
+ case "step:tool_call":
5597
+ console.log(chalk4.cyan(` [step ${event.stepNumber}] ${event.toolName}${event.toolInput ? ` ${formatToolInput(event.toolInput)}` : ""}`));
5598
+ break;
5599
+ case "step:tool_result":
5600
+ if (event.toolName === "report_result") {
5601
+ console.log(chalk4.bold(` [result] ${event.toolResult}`));
5602
+ } else {
5603
+ const resultPreview = (event.toolResult ?? "").length > 100 ? (event.toolResult ?? "").slice(0, 100) + "..." : event.toolResult ?? "";
5604
+ console.log(chalk4.dim(` [done] ${resultPreview}`));
5605
+ }
5606
+ break;
5607
+ case "screenshot:captured":
5608
+ console.log(chalk4.dim(` [screenshot] ${event.screenshotPath}`));
5609
+ break;
5610
+ case "scenario:pass":
5611
+ console.log(chalk4.green(` [PASS] ${event.scenarioName}`));
5612
+ break;
5613
+ case "scenario:fail":
5614
+ console.log(chalk4.red(` [FAIL] ${event.scenarioName}`));
5615
+ break;
5616
+ case "scenario:error":
5617
+ console.log(chalk4.yellow(` [ERR] ${event.scenarioName}: ${event.error}`));
5618
+ break;
5619
+ }
5620
+ });
5621
+ console.log("");
5622
+ console.log(chalk4.bold(` Running tests against ${url}`));
5623
+ console.log("");
5624
+ }
5448
5625
  if (description) {
5449
5626
  const scenario = createScenario({
5450
5627
  name: description,
@@ -5475,10 +5652,6 @@ program2.command("run <url> [description]").description("Run test scenarios agai
5475
5652
  }
5476
5653
  process.exit(getExitCode(run2));
5477
5654
  }
5478
- if (opts.fromTodos) {
5479
- const result = importFromTodos({ projectId });
5480
- console.log(chalk4.blue(`Imported ${result.imported} scenarios from todos (${result.skipped} skipped)`));
5481
- }
5482
5655
  const { run, results } = await runByFilter({
5483
5656
  url,
5484
5657
  tags: opts.tag.length > 0 ? opts.tag : undefined,
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export { loadConfig, resolveModel as resolveModelConfig, getDefaultConfig, } fro
12
12
  export { launchBrowser, getPage, closeBrowser, BrowserPool, installBrowser, } from "./lib/browser.js";
13
13
  export { Screenshotter, slugify, generateFilename, getScreenshotDir, ensureDir, } from "./lib/screenshotter.js";
14
14
  export { createClient, resolveModel, runAgentLoop, executeTool, BROWSER_TOOLS, } from "./lib/ai-client.js";
15
- export { runSingleScenario, runBatch, runByFilter, onRunEvent, } from "./lib/runner.js";
15
+ export { runSingleScenario, runBatch, runByFilter, startRunAsync, onRunEvent, } from "./lib/runner.js";
16
16
  export type { RunOptions, RunEvent, RunEventHandler } from "./lib/runner.js";
17
17
  export { formatTerminal, formatJSON, formatSummary, getExitCode, formatRunList, formatScenarioList, formatResultDetail, } from "./lib/reporter.js";
18
18
  export { connectToTodos, pullTasks, taskToScenarioInput, importFromTodos, markTodoDone, } from "./lib/todos-connector.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,WAAW,EACX,UAAU,EACV,QAAQ,EACR,WAAW,EACX,MAAM,EACN,SAAS,EACT,aAAa,EACb,OAAO,EACP,KAAK,EACL,QAAQ,EACR,GAAG,EACH,MAAM,EACN,UAAU,EACV,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,SAAS,EACT,WAAW,EACX,QAAQ,EACR,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACd,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,aAAa,GACd,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,SAAS,EACT,cAAc,EACd,YAAY,EACZ,eAAe,EACf,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,qBAAqB,EACrB,gBAAgB,EAChB,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,WAAW,EACX,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,GAAG,EACH,IAAI,EACJ,SAAS,GACV,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,cAAc,EACd,WAAW,EACX,oBAAoB,EACpB,aAAa,EACb,cAAc,EACd,cAAc,GACf,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,SAAS,EACT,MAAM,EACN,QAAQ,EACR,SAAS,EACT,SAAS,GACV,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,YAAY,EACZ,SAAS,EACT,WAAW,EACX,YAAY,EACZ,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,sBAAsB,GACvB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,aAAa,EACb,QAAQ,EACR,cAAc,EACd,UAAU,GACX,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,cAAc,EACd,WAAW,EACX,aAAa,EACb,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,aAAa,GACd,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,UAAU,EACV,YAAY,IAAI,kBAAkB,EAClC,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,aAAa,EACb,OAAO,EACP,YAAY,EACZ,WAAW,EACX,cAAc,GACf,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,aAAa,EACb,OAAO,EACP,gBAAgB,EAChB,gBAAgB,EAChB,SAAS,GACV,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,aAAa,GACd,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,iBAAiB,EACjB,QAAQ,EACR,WAAW,EACX,UAAU,GACX,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAE7E,OAAO,EACL,cAAc,EACd,UAAU,EACV,aAAa,EACb,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,cAAc,EACd,SAAS,EACT,mBAAmB,EACnB,eAAe,EACf,YAAY,GACb,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,SAAS,EACT,SAAS,EACT,cAAc,EACd,WAAW,EACX,cAAc,GACf,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EACL,WAAW,EACX,eAAe,EACf,mBAAmB,GACpB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE9D,OAAO,EACL,QAAQ,EACR,kBAAkB,EAClB,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE9D,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,aAAa,GACd,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEhE,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,EACL,aAAa,EACb,UAAU,EACV,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,WAAW,GACZ,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEjE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AACzE,YAAY,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1951,7 +1951,8 @@ async function runAgentLoop(options) {
1951
1951
  screenshotter,
1952
1952
  model,
1953
1953
  runId,
1954
- maxTurns = 30
1954
+ maxTurns = 30,
1955
+ onStep
1955
1956
  } = options;
1956
1957
  const systemPrompt = [
1957
1958
  "You are an expert QA testing agent. Your job is to thoroughly test web application scenarios.",
@@ -2010,8 +2011,8 @@ async function runAgentLoop(options) {
2010
2011
  }
2011
2012
  const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
2012
2013
  if (toolUseBlocks.length === 0 && response.stop_reason === "end_turn") {
2013
- const textBlocks = response.content.filter((block) => block.type === "text");
2014
- const textReasoning = textBlocks.map((b) => b.text).join(`
2014
+ const textBlocks2 = response.content.filter((block) => block.type === "text");
2015
+ const textReasoning = textBlocks2.map((b) => b.text).join(`
2015
2016
  `);
2016
2017
  return {
2017
2018
  status: "error",
@@ -2022,10 +2023,22 @@ async function runAgentLoop(options) {
2022
2023
  };
2023
2024
  }
2024
2025
  const toolResults = [];
2026
+ const textBlocks = response.content.filter((block) => block.type === "text");
2027
+ if (textBlocks.length > 0 && onStep) {
2028
+ const thinking = textBlocks.map((b) => b.text).join(`
2029
+ `);
2030
+ onStep({ type: "thinking", thinking, stepNumber });
2031
+ }
2025
2032
  for (const toolBlock of toolUseBlocks) {
2026
2033
  stepNumber++;
2027
2034
  const toolInput = toolBlock.input;
2035
+ if (onStep) {
2036
+ onStep({ type: "tool_call", toolName: toolBlock.name, toolInput, stepNumber });
2037
+ }
2028
2038
  const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber });
2039
+ if (onStep) {
2040
+ onStep({ type: "tool_result", toolName: toolBlock.name, toolResult: execResult.result, stepNumber });
2041
+ }
2029
2042
  if (execResult.screenshot) {
2030
2043
  screenshots.push({
2031
2044
  ...execResult.screenshot,
@@ -2115,7 +2128,20 @@ async function runSingleScenario(scenario, runId, options) {
2115
2128
  screenshotter,
2116
2129
  model,
2117
2130
  runId,
2118
- maxTurns: 30
2131
+ maxTurns: 30,
2132
+ onStep: (stepEvent) => {
2133
+ emit({
2134
+ type: `step:${stepEvent.type}`,
2135
+ scenarioId: scenario.id,
2136
+ scenarioName: scenario.name,
2137
+ runId,
2138
+ toolName: stepEvent.toolName,
2139
+ toolInput: stepEvent.toolInput,
2140
+ toolResult: stepEvent.toolResult,
2141
+ thinking: stepEvent.thinking,
2142
+ stepNumber: stepEvent.stepNumber
2143
+ });
2144
+ }
2119
2145
  });
2120
2146
  for (const ss of agentResult.screenshots) {
2121
2147
  createScreenshot({
@@ -2225,6 +2251,79 @@ async function runByFilter(options) {
2225
2251
  }
2226
2252
  return runBatch(scenarios, options);
2227
2253
  }
2254
+ function startRunAsync(options) {
2255
+ const config = loadConfig();
2256
+ const model = resolveModel2(options.model ?? config.defaultModel);
2257
+ let scenarios;
2258
+ if (options.scenarioIds && options.scenarioIds.length > 0) {
2259
+ const all = listScenarios({ projectId: options.projectId });
2260
+ scenarios = all.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
2261
+ } else {
2262
+ scenarios = listScenarios({
2263
+ projectId: options.projectId,
2264
+ tags: options.tags,
2265
+ priority: options.priority
2266
+ });
2267
+ }
2268
+ const parallel = options.parallel ?? 1;
2269
+ const run = createRun({
2270
+ url: options.url,
2271
+ model,
2272
+ headed: options.headed,
2273
+ parallel,
2274
+ projectId: options.projectId
2275
+ });
2276
+ if (scenarios.length === 0) {
2277
+ updateRun(run.id, { status: "passed", total: 0, finished_at: new Date().toISOString() });
2278
+ return { runId: run.id, scenarioCount: 0 };
2279
+ }
2280
+ updateRun(run.id, { status: "running", total: scenarios.length });
2281
+ (async () => {
2282
+ const results = [];
2283
+ try {
2284
+ if (parallel <= 1) {
2285
+ for (const scenario of scenarios) {
2286
+ const result = await runSingleScenario(scenario, run.id, options);
2287
+ results.push(result);
2288
+ }
2289
+ } else {
2290
+ const queue = [...scenarios];
2291
+ const running = [];
2292
+ const processNext = async () => {
2293
+ const scenario = queue.shift();
2294
+ if (!scenario)
2295
+ return;
2296
+ const result = await runSingleScenario(scenario, run.id, options);
2297
+ results.push(result);
2298
+ await processNext();
2299
+ };
2300
+ const workers = Math.min(parallel, scenarios.length);
2301
+ for (let i = 0;i < workers; i++) {
2302
+ running.push(processNext());
2303
+ }
2304
+ await Promise.all(running);
2305
+ }
2306
+ const passed = results.filter((r) => r.status === "passed").length;
2307
+ const failed = results.filter((r) => r.status === "failed" || r.status === "error").length;
2308
+ updateRun(run.id, {
2309
+ status: failed > 0 ? "failed" : "passed",
2310
+ passed,
2311
+ failed,
2312
+ total: scenarios.length,
2313
+ finished_at: new Date().toISOString()
2314
+ });
2315
+ emit({ type: "run:complete", runId: run.id });
2316
+ } catch (error) {
2317
+ const errorMsg = error instanceof Error ? error.message : String(error);
2318
+ updateRun(run.id, {
2319
+ status: "failed",
2320
+ finished_at: new Date().toISOString()
2321
+ });
2322
+ emit({ type: "run:complete", runId: run.id, error: errorMsg });
2323
+ }
2324
+ })();
2325
+ return { runId: run.id, scenarioCount: scenarios.length };
2326
+ }
2228
2327
  function estimateCost(model, tokens) {
2229
2328
  const costs = {
2230
2329
  "claude-haiku-4-5-20251001": 0.1,
@@ -4418,6 +4517,7 @@ export {
4418
4517
  testWebhook,
4419
4518
  taskToScenarioInput,
4420
4519
  startWatcher,
4520
+ startRunAsync,
4421
4521
  slugify,
4422
4522
  shouldRunAt,
4423
4523
  shortUuid,
@@ -31,6 +31,14 @@ interface ToolExecutionResult {
31
31
  * Returns the result string and an optional screenshot capture.
32
32
  */
33
33
  export declare function executeTool(page: Page, screenshotter: Screenshotter, toolName: string, toolInput: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult>;
34
+ export type StepEventHandler = (event: {
35
+ type: "tool_call" | "tool_result" | "thinking";
36
+ toolName?: string;
37
+ toolInput?: Record<string, unknown>;
38
+ toolResult?: string;
39
+ thinking?: string;
40
+ stepNumber: number;
41
+ }) => void;
34
42
  interface AgentLoopOptions {
35
43
  client: Anthropic;
36
44
  page: Page;
@@ -39,6 +47,7 @@ interface AgentLoopOptions {
39
47
  model: string;
40
48
  runId: string;
41
49
  maxTurns?: number;
50
+ onStep?: StepEventHandler;
42
51
  }
43
52
  interface AgentLoopResult {
44
53
  status: "passed" | "failed" | "error";
@@ -1 +1 @@
1
- {"version":3,"file":"ai-client.d.ts","sourceRoot":"","sources":["../../src/lib/ai-client.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAC1C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,KAAK,EAAe,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAI/D;;;GAGG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAKzD;AAID,eAAO,MAAM,aAAa,EAAE,SAAS,CAAC,IAAI,EAyTzC,CAAC;AAIF,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,gBAAgB;IACxB,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;AAED,UAAU,mBAAmB;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,IAAI,EACV,aAAa,EAAE,aAAa,EAC5B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,mBAAmB,CAAC,CA8P9B;AAID,UAAU,gBAAgB;IACxB,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,eAAe;IACvB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,KAAK,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;KAC9B,CAAC,CAAC;CACJ;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,eAAe,CAAC,CAyK1B;AAID;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAQvD"}
1
+ {"version":3,"file":"ai-client.d.ts","sourceRoot":"","sources":["../../src/lib/ai-client.ts"],"names":[],"mappings":"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"}
@@ -10,13 +10,18 @@ export interface RunOptions {
10
10
  screenshotDir?: string;
11
11
  }
12
12
  export interface RunEvent {
13
- type: "scenario:start" | "scenario:pass" | "scenario:fail" | "scenario:error" | "screenshot:captured" | "run:complete";
13
+ type: "scenario:start" | "scenario:pass" | "scenario:fail" | "scenario:error" | "screenshot:captured" | "run:complete" | "step:tool_call" | "step:tool_result" | "step:thinking";
14
14
  scenarioId?: string;
15
15
  scenarioName?: string;
16
16
  resultId?: string;
17
17
  runId?: string;
18
18
  error?: string;
19
19
  screenshotPath?: string;
20
+ toolName?: string;
21
+ toolInput?: Record<string, unknown>;
22
+ toolResult?: string;
23
+ thinking?: string;
24
+ stepNumber?: number;
20
25
  }
21
26
  export type RunEventHandler = (event: RunEvent) => void;
22
27
  export declare function onRunEvent(handler: RunEventHandler): void;
@@ -33,4 +38,16 @@ export declare function runByFilter(options: RunOptions & {
33
38
  run: Run;
34
39
  results: Result[];
35
40
  }>;
41
+ /**
42
+ * Start a run asynchronously — creates the run record immediately and returns it,
43
+ * then executes scenarios in the background. Poll getRun(id) to check progress.
44
+ */
45
+ export declare function startRunAsync(options: RunOptions & {
46
+ tags?: string[];
47
+ priority?: string;
48
+ scenarioIds?: string[];
49
+ }): {
50
+ runId: string;
51
+ scenarioCount: number;
52
+ };
36
53
  //# sourceMappingURL=runner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAW/D,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,gBAAgB,GAAG,eAAe,GAAG,eAAe,GAAG,gBAAgB,GAAG,qBAAqB,GAAG,cAAc,CAAC;IACvH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;AAIxD,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAEzD;AAMD,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,MAAM,CAAC,CAoFjB;AAED,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,QAAQ,EAAE,EACrB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA4D1C;AAED,wBAAsB,WAAW,CAC/B,OAAO,EAAE,UAAU,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GACnF,OAAO,CAAC;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAuB1C"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAW/D,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EACA,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,gBAAgB,GAChB,qBAAqB,GACrB,cAAc,GACd,gBAAgB,GAChB,kBAAkB,GAClB,eAAe,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;AAIxD,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAEzD;AAMD,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,MAAM,CAAC,CAiGjB;AAED,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,QAAQ,EAAE,EACrB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA4D1C;AAED,wBAAsB,WAAW,CAC/B,OAAO,EAAE,UAAU,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GACnF,OAAO,CAAC;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAuB1C;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,UAAU,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GACnF;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CA+E1C"}
package/dist/mcp/index.js CHANGED
@@ -5565,7 +5565,8 @@ async function runAgentLoop(options) {
5565
5565
  screenshotter,
5566
5566
  model,
5567
5567
  runId,
5568
- maxTurns = 30
5568
+ maxTurns = 30,
5569
+ onStep
5569
5570
  } = options;
5570
5571
  const systemPrompt = [
5571
5572
  "You are an expert QA testing agent. Your job is to thoroughly test web application scenarios.",
@@ -5624,8 +5625,8 @@ async function runAgentLoop(options) {
5624
5625
  }
5625
5626
  const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
5626
5627
  if (toolUseBlocks.length === 0 && response.stop_reason === "end_turn") {
5627
- const textBlocks = response.content.filter((block) => block.type === "text");
5628
- const textReasoning = textBlocks.map((b) => b.text).join(`
5628
+ const textBlocks2 = response.content.filter((block) => block.type === "text");
5629
+ const textReasoning = textBlocks2.map((b) => b.text).join(`
5629
5630
  `);
5630
5631
  return {
5631
5632
  status: "error",
@@ -5636,10 +5637,22 @@ async function runAgentLoop(options) {
5636
5637
  };
5637
5638
  }
5638
5639
  const toolResults = [];
5640
+ const textBlocks = response.content.filter((block) => block.type === "text");
5641
+ if (textBlocks.length > 0 && onStep) {
5642
+ const thinking = textBlocks.map((b) => b.text).join(`
5643
+ `);
5644
+ onStep({ type: "thinking", thinking, stepNumber });
5645
+ }
5639
5646
  for (const toolBlock of toolUseBlocks) {
5640
5647
  stepNumber++;
5641
5648
  const toolInput = toolBlock.input;
5649
+ if (onStep) {
5650
+ onStep({ type: "tool_call", toolName: toolBlock.name, toolInput, stepNumber });
5651
+ }
5642
5652
  const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber });
5653
+ if (onStep) {
5654
+ onStep({ type: "tool_result", toolName: toolBlock.name, toolResult: execResult.result, stepNumber });
5655
+ }
5643
5656
  if (execResult.screenshot) {
5644
5657
  screenshots.push({
5645
5658
  ...execResult.screenshot,
@@ -5781,7 +5794,20 @@ async function runSingleScenario(scenario, runId, options) {
5781
5794
  screenshotter,
5782
5795
  model,
5783
5796
  runId,
5784
- maxTurns: 30
5797
+ maxTurns: 30,
5798
+ onStep: (stepEvent) => {
5799
+ emit({
5800
+ type: `step:${stepEvent.type}`,
5801
+ scenarioId: scenario.id,
5802
+ scenarioName: scenario.name,
5803
+ runId,
5804
+ toolName: stepEvent.toolName,
5805
+ toolInput: stepEvent.toolInput,
5806
+ toolResult: stepEvent.toolResult,
5807
+ thinking: stepEvent.thinking,
5808
+ stepNumber: stepEvent.stepNumber
5809
+ });
5810
+ }
5785
5811
  });
5786
5812
  for (const ss of agentResult.screenshots) {
5787
5813
  createScreenshot({
@@ -5891,6 +5917,79 @@ async function runByFilter(options) {
5891
5917
  }
5892
5918
  return runBatch(scenarios, options);
5893
5919
  }
5920
+ function startRunAsync(options) {
5921
+ const config = loadConfig();
5922
+ const model = resolveModel(options.model ?? config.defaultModel);
5923
+ let scenarios;
5924
+ if (options.scenarioIds && options.scenarioIds.length > 0) {
5925
+ const all = listScenarios({ projectId: options.projectId });
5926
+ scenarios = all.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
5927
+ } else {
5928
+ scenarios = listScenarios({
5929
+ projectId: options.projectId,
5930
+ tags: options.tags,
5931
+ priority: options.priority
5932
+ });
5933
+ }
5934
+ const parallel = options.parallel ?? 1;
5935
+ const run = createRun({
5936
+ url: options.url,
5937
+ model,
5938
+ headed: options.headed,
5939
+ parallel,
5940
+ projectId: options.projectId
5941
+ });
5942
+ if (scenarios.length === 0) {
5943
+ updateRun(run.id, { status: "passed", total: 0, finished_at: new Date().toISOString() });
5944
+ return { runId: run.id, scenarioCount: 0 };
5945
+ }
5946
+ updateRun(run.id, { status: "running", total: scenarios.length });
5947
+ (async () => {
5948
+ const results = [];
5949
+ try {
5950
+ if (parallel <= 1) {
5951
+ for (const scenario of scenarios) {
5952
+ const result = await runSingleScenario(scenario, run.id, options);
5953
+ results.push(result);
5954
+ }
5955
+ } else {
5956
+ const queue = [...scenarios];
5957
+ const running = [];
5958
+ const processNext = async () => {
5959
+ const scenario = queue.shift();
5960
+ if (!scenario)
5961
+ return;
5962
+ const result = await runSingleScenario(scenario, run.id, options);
5963
+ results.push(result);
5964
+ await processNext();
5965
+ };
5966
+ const workers = Math.min(parallel, scenarios.length);
5967
+ for (let i = 0;i < workers; i++) {
5968
+ running.push(processNext());
5969
+ }
5970
+ await Promise.all(running);
5971
+ }
5972
+ const passed = results.filter((r) => r.status === "passed").length;
5973
+ const failed = results.filter((r) => r.status === "failed" || r.status === "error").length;
5974
+ updateRun(run.id, {
5975
+ status: failed > 0 ? "failed" : "passed",
5976
+ passed,
5977
+ failed,
5978
+ total: scenarios.length,
5979
+ finished_at: new Date().toISOString()
5980
+ });
5981
+ emit({ type: "run:complete", runId: run.id });
5982
+ } catch (error) {
5983
+ const errorMsg = error instanceof Error ? error.message : String(error);
5984
+ updateRun(run.id, {
5985
+ status: "failed",
5986
+ finished_at: new Date().toISOString()
5987
+ });
5988
+ emit({ type: "run:complete", runId: run.id, error: errorMsg });
5989
+ }
5990
+ })();
5991
+ return { runId: run.id, scenarioCount: scenarios.length };
5992
+ }
5894
5993
  function estimateCost(model, tokens) {
5895
5994
  const costs = {
5896
5995
  "claude-haiku-4-5-20251001": 0.1,
@@ -6343,7 +6442,7 @@ class Scheduler {
6343
6442
 
6344
6443
  // src/mcp/index.ts
6345
6444
  var server = new McpServer({
6346
- name: "testers-mcp",
6445
+ name: "testers",
6347
6446
  version: "0.0.1"
6348
6447
  });
6349
6448
  server.tool("create_scenario", "Create a new test scenario", {
@@ -6457,18 +6556,15 @@ server.tool("run_scenarios", "Run test scenarios against a URL", {
6457
6556
  parallel: exports_external.number().optional().describe("Number of parallel workers")
6458
6557
  }, async ({ url, tags, scenarioIds, priority, model, headed, parallel }) => {
6459
6558
  try {
6460
- const { run, results } = await runByFilter({ url, tags, scenarioIds, priority, model, headed, parallel });
6461
- const passed = results.filter((r) => r.status === "passed").length;
6462
- const failed = results.filter((r) => r.status === "failed" || r.status === "error").length;
6463
- const skipped = results.filter((r) => r.status === "skipped").length;
6559
+ const { runId, scenarioCount } = startRunAsync({ url, tags, scenarioIds, priority, model, headed, parallel });
6464
6560
  const text = [
6465
- `Run ${run.id} \u2014 ${run.status}`,
6466
- `URL: ${run.url}`,
6467
- `Total: ${results.length} | Passed: ${passed} | Failed: ${failed} | Skipped: ${skipped}`,
6468
- `Model: ${run.model}`,
6469
- `Started: ${run.startedAt}`,
6470
- run.finishedAt ? `Finished: ${run.finishedAt}` : null
6471
- ].filter(Boolean).join(`
6561
+ `Run started: ${runId}`,
6562
+ `Scenarios: ${scenarioCount}`,
6563
+ `URL: ${url}`,
6564
+ `Status: running (async)`,
6565
+ ``,
6566
+ `Poll with get_run to check progress.`
6567
+ ].join(`
6472
6568
  `);
6473
6569
  return { content: [{ type: "text", text }] };
6474
6570
  } catch (error) {
@@ -6712,6 +6808,6 @@ async function main() {
6712
6808
  await server.connect(transport);
6713
6809
  }
6714
6810
  main().catch((error) => {
6715
- console.error("Failed to start testers-mcp:", error);
6811
+ console.error("Failed to start testers:", error);
6716
6812
  process.exit(1);
6717
6813
  });
@@ -1499,7 +1499,8 @@ async function runAgentLoop(options) {
1499
1499
  screenshotter,
1500
1500
  model,
1501
1501
  runId,
1502
- maxTurns = 30
1502
+ maxTurns = 30,
1503
+ onStep
1503
1504
  } = options;
1504
1505
  const systemPrompt = [
1505
1506
  "You are an expert QA testing agent. Your job is to thoroughly test web application scenarios.",
@@ -1558,8 +1559,8 @@ async function runAgentLoop(options) {
1558
1559
  }
1559
1560
  const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
1560
1561
  if (toolUseBlocks.length === 0 && response.stop_reason === "end_turn") {
1561
- const textBlocks = response.content.filter((block) => block.type === "text");
1562
- const textReasoning = textBlocks.map((b) => b.text).join(`
1562
+ const textBlocks2 = response.content.filter((block) => block.type === "text");
1563
+ const textReasoning = textBlocks2.map((b) => b.text).join(`
1563
1564
  `);
1564
1565
  return {
1565
1566
  status: "error",
@@ -1570,10 +1571,22 @@ async function runAgentLoop(options) {
1570
1571
  };
1571
1572
  }
1572
1573
  const toolResults = [];
1574
+ const textBlocks = response.content.filter((block) => block.type === "text");
1575
+ if (textBlocks.length > 0 && onStep) {
1576
+ const thinking = textBlocks.map((b) => b.text).join(`
1577
+ `);
1578
+ onStep({ type: "thinking", thinking, stepNumber });
1579
+ }
1573
1580
  for (const toolBlock of toolUseBlocks) {
1574
1581
  stepNumber++;
1575
1582
  const toolInput = toolBlock.input;
1583
+ if (onStep) {
1584
+ onStep({ type: "tool_call", toolName: toolBlock.name, toolInput, stepNumber });
1585
+ }
1576
1586
  const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber });
1587
+ if (onStep) {
1588
+ onStep({ type: "tool_result", toolName: toolBlock.name, toolResult: execResult.result, stepNumber });
1589
+ }
1577
1590
  if (execResult.screenshot) {
1578
1591
  screenshots.push({
1579
1592
  ...execResult.screenshot,
@@ -1715,7 +1728,20 @@ async function runSingleScenario(scenario, runId, options) {
1715
1728
  screenshotter,
1716
1729
  model,
1717
1730
  runId,
1718
- maxTurns: 30
1731
+ maxTurns: 30,
1732
+ onStep: (stepEvent) => {
1733
+ emit({
1734
+ type: `step:${stepEvent.type}`,
1735
+ scenarioId: scenario.id,
1736
+ scenarioName: scenario.name,
1737
+ runId,
1738
+ toolName: stepEvent.toolName,
1739
+ toolInput: stepEvent.toolInput,
1740
+ toolResult: stepEvent.toolResult,
1741
+ thinking: stepEvent.thinking,
1742
+ stepNumber: stepEvent.stepNumber
1743
+ });
1744
+ }
1719
1745
  });
1720
1746
  for (const ss of agentResult.screenshots) {
1721
1747
  createScreenshot({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/testers",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
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",
@@ -28,7 +28,7 @@
28
28
  "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright",
29
29
  "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --external @anthropic-ai/sdk --external playwright",
30
30
  "build:lib": "bun build src/index.ts --outdir dist --target bun --external playwright --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk",
31
- "build:types": "tsc --emitDeclarationOnly --outDir dist",
31
+ "build:types": "NODE_OPTIONS='--max-old-space-size=8192' tsc --emitDeclarationOnly --outDir dist --skipLibCheck",
32
32
  "build:dashboard": "cd dashboard && bun run build",
33
33
  "typecheck": "tsc --noEmit",
34
34
  "test": "bun test",