@hasna/testers 0.0.59 → 0.0.61

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
@@ -27425,18 +27425,46 @@ function buildSandboxCommand(input) {
27425
27425
  "--no-auto-generate",
27426
27426
  "--json"
27427
27427
  ];
27428
+ const installBrowserArgs = [
27429
+ "bunx",
27430
+ input.packageSpec,
27431
+ "install-browser",
27432
+ "--engine",
27433
+ "playwright"
27434
+ ];
27428
27435
  return [
27429
27436
  "set -euo pipefail",
27437
+ buildBunBootstrapCommand(),
27430
27438
  `mkdir -p ${shellQuote(input.remoteDir)}`,
27431
27439
  `mkdir -p ${shellQuote(input.stateRemoteDir)}`,
27432
27440
  input.appRemoteDir ? `mkdir -p ${shellQuote(input.appRemoteDir)}` : undefined,
27433
27441
  `cd ${shellQuote(input.appRemoteDir ?? input.remoteDir)}`,
27434
27442
  input.setupCommand,
27435
27443
  buildAppStartCommand(input),
27444
+ buildSandboxBrowserInstallCommand(installBrowserArgs),
27436
27445
  `HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
27437
27446
  ].filter(Boolean).join(`
27438
27447
  `);
27439
27448
  }
27449
+ function buildBunBootstrapCommand() {
27450
+ return [
27451
+ 'export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"',
27452
+ 'export PATH="$BUN_INSTALL/bin:$PATH"',
27453
+ "if ! command -v bun >/dev/null 2>&1; then",
27454
+ " curl -fsSL https://bun.sh/install | bash",
27455
+ "fi",
27456
+ "command -v bun >/dev/null 2>&1"
27457
+ ].join(`
27458
+ `);
27459
+ }
27460
+ function buildSandboxBrowserInstallCommand(args) {
27461
+ return [
27462
+ 'if [ "${TESTERS_SANDBOX_SKIP_BROWSER_INSTALL:-}" != "1" ]; then',
27463
+ ` ${args.map(shellQuote).join(" ")}`,
27464
+ "fi"
27465
+ ].join(`
27466
+ `);
27467
+ }
27440
27468
  function buildAppStartCommand(input) {
27441
27469
  if (!input.appStartCommand)
27442
27470
  return;
@@ -27474,14 +27502,17 @@ async function runViaSandbox(plan, dependencies) {
27474
27502
  const sandboxes = await resolveSandboxesRuntime(dependencies);
27475
27503
  const createBundle = dependencies.createDatabaseBundle ?? createWorkflowDatabaseBundle;
27476
27504
  const bundle = createBundle(plan.workflow, plan);
27505
+ const sandboxTimeoutSeconds = plan.sandbox.timeoutMs === undefined ? undefined : Math.ceil(plan.sandbox.timeoutMs / 1000);
27506
+ let capturedStdout = "";
27507
+ let capturedStderr = "";
27477
27508
  try {
27478
27509
  const raw = await sandboxes.runCommandInSandbox({
27479
27510
  command: plan.sandbox.command,
27480
27511
  provider: plan.sandbox.provider,
27481
27512
  name: plan.sandbox.name,
27482
27513
  image: plan.sandbox.image,
27483
- sandboxTimeout: plan.sandbox.timeoutMs,
27484
- commandTimeoutMs: plan.sandbox.timeoutMs,
27514
+ sandboxTimeout: sandboxTimeoutSeconds,
27515
+ commandTimeoutMs: sandboxTimeoutSeconds,
27485
27516
  config: {
27486
27517
  source: "testers",
27487
27518
  testersProjectId: plan.workflow.projectId ?? undefined,
@@ -27494,6 +27525,12 @@ async function runViaSandbox(plan, dependencies) {
27494
27525
  localDir: bundle.localDir,
27495
27526
  remoteDir: bundle.remoteDir,
27496
27527
  syncStrategy: plan.sandbox.syncStrategy
27528
+ },
27529
+ onStdout: (data) => {
27530
+ capturedStdout = appendCapturedSandboxOutput(capturedStdout, data);
27531
+ },
27532
+ onStderr: (data) => {
27533
+ capturedStderr = appendCapturedSandboxOutput(capturedStderr, data);
27497
27534
  }
27498
27535
  });
27499
27536
  const exitCode = raw.result.exit_code ?? raw.result.exitCode ?? 0;
@@ -27510,10 +27547,33 @@ async function runViaSandbox(plan, dependencies) {
27510
27547
  stderr,
27511
27548
  cleanup: raw.cleanup
27512
27549
  };
27550
+ } catch (error) {
27551
+ if (capturedStdout || capturedStderr) {
27552
+ throw buildSandboxStreamError(error, capturedStdout, capturedStderr);
27553
+ }
27554
+ throw error;
27513
27555
  } finally {
27514
27556
  bundle.cleanup?.();
27515
27557
  }
27516
27558
  }
27559
+ function appendCapturedSandboxOutput(current, data) {
27560
+ const next = current + data;
27561
+ if (next.length <= MAX_CAPTURED_SANDBOX_OUTPUT)
27562
+ return next;
27563
+ return next.slice(next.length - MAX_CAPTURED_SANDBOX_OUTPUT);
27564
+ }
27565
+ function buildSandboxStreamError(error, stdout, stderr) {
27566
+ const message = error instanceof Error ? error.message : String(error);
27567
+ const parts = [`Sandbox workflow execution failed: ${message}`];
27568
+ if (stdout.trim())
27569
+ parts.push(`stdout:
27570
+ ${stdout.trimEnd()}`);
27571
+ if (stderr.trim())
27572
+ parts.push(`stderr:
27573
+ ${stderr.trimEnd()}`);
27574
+ return new Error(parts.join(`
27575
+ `));
27576
+ }
27517
27577
  function resolveSandboxEnv(env) {
27518
27578
  if (!env || Object.keys(env).length === 0)
27519
27579
  return;
@@ -27545,7 +27605,7 @@ async function resolveSandboxesRuntime(dependencies) {
27545
27605
  function shellQuote(value) {
27546
27606
  return `'${value.replaceAll("'", `'"'"'`)}'`;
27547
27607
  }
27548
- var APP_SOURCE_EXCLUDES;
27608
+ var APP_SOURCE_EXCLUDES, MAX_CAPTURED_SANDBOX_OUTPUT = 120000;
27549
27609
  var init_workflow_runner = __esm(() => {
27550
27610
  init_database();
27551
27611
  init_workflows();
@@ -95691,7 +95751,7 @@ import chalk6 from "chalk";
95691
95751
  // package.json
95692
95752
  var package_default = {
95693
95753
  name: "@hasna/testers",
95694
- version: "0.0.59",
95754
+ version: "0.0.61",
95695
95755
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
95696
95756
  type: "module",
95697
95757
  main: "dist/index.js",
package/dist/index.js CHANGED
@@ -17384,6 +17384,7 @@ var APP_SOURCE_EXCLUDES = [
17384
17384
  ".venv",
17385
17385
  "__pycache__"
17386
17386
  ];
17387
+ var MAX_CAPTURED_SANDBOX_OUTPUT = 120000;
17387
17388
  function buildWorkflowRunPlan(workflow, options) {
17388
17389
  const runOptions = {
17389
17390
  url: options.url,
@@ -17540,18 +17541,46 @@ function buildSandboxCommand(input) {
17540
17541
  "--no-auto-generate",
17541
17542
  "--json"
17542
17543
  ];
17544
+ const installBrowserArgs = [
17545
+ "bunx",
17546
+ input.packageSpec,
17547
+ "install-browser",
17548
+ "--engine",
17549
+ "playwright"
17550
+ ];
17543
17551
  return [
17544
17552
  "set -euo pipefail",
17553
+ buildBunBootstrapCommand(),
17545
17554
  `mkdir -p ${shellQuote(input.remoteDir)}`,
17546
17555
  `mkdir -p ${shellQuote(input.stateRemoteDir)}`,
17547
17556
  input.appRemoteDir ? `mkdir -p ${shellQuote(input.appRemoteDir)}` : undefined,
17548
17557
  `cd ${shellQuote(input.appRemoteDir ?? input.remoteDir)}`,
17549
17558
  input.setupCommand,
17550
17559
  buildAppStartCommand(input),
17560
+ buildSandboxBrowserInstallCommand(installBrowserArgs),
17551
17561
  `HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
17552
17562
  ].filter(Boolean).join(`
17553
17563
  `);
17554
17564
  }
17565
+ function buildBunBootstrapCommand() {
17566
+ return [
17567
+ 'export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"',
17568
+ 'export PATH="$BUN_INSTALL/bin:$PATH"',
17569
+ "if ! command -v bun >/dev/null 2>&1; then",
17570
+ " curl -fsSL https://bun.sh/install | bash",
17571
+ "fi",
17572
+ "command -v bun >/dev/null 2>&1"
17573
+ ].join(`
17574
+ `);
17575
+ }
17576
+ function buildSandboxBrowserInstallCommand(args) {
17577
+ return [
17578
+ 'if [ "${TESTERS_SANDBOX_SKIP_BROWSER_INSTALL:-}" != "1" ]; then',
17579
+ ` ${args.map(shellQuote).join(" ")}`,
17580
+ "fi"
17581
+ ].join(`
17582
+ `);
17583
+ }
17555
17584
  function buildAppStartCommand(input) {
17556
17585
  if (!input.appStartCommand)
17557
17586
  return;
@@ -17589,14 +17618,17 @@ async function runViaSandbox(plan, dependencies) {
17589
17618
  const sandboxes = await resolveSandboxesRuntime(dependencies);
17590
17619
  const createBundle = dependencies.createDatabaseBundle ?? createWorkflowDatabaseBundle;
17591
17620
  const bundle = createBundle(plan.workflow, plan);
17621
+ const sandboxTimeoutSeconds = plan.sandbox.timeoutMs === undefined ? undefined : Math.ceil(plan.sandbox.timeoutMs / 1000);
17622
+ let capturedStdout = "";
17623
+ let capturedStderr = "";
17592
17624
  try {
17593
17625
  const raw = await sandboxes.runCommandInSandbox({
17594
17626
  command: plan.sandbox.command,
17595
17627
  provider: plan.sandbox.provider,
17596
17628
  name: plan.sandbox.name,
17597
17629
  image: plan.sandbox.image,
17598
- sandboxTimeout: plan.sandbox.timeoutMs,
17599
- commandTimeoutMs: plan.sandbox.timeoutMs,
17630
+ sandboxTimeout: sandboxTimeoutSeconds,
17631
+ commandTimeoutMs: sandboxTimeoutSeconds,
17600
17632
  config: {
17601
17633
  source: "testers",
17602
17634
  testersProjectId: plan.workflow.projectId ?? undefined,
@@ -17609,6 +17641,12 @@ async function runViaSandbox(plan, dependencies) {
17609
17641
  localDir: bundle.localDir,
17610
17642
  remoteDir: bundle.remoteDir,
17611
17643
  syncStrategy: plan.sandbox.syncStrategy
17644
+ },
17645
+ onStdout: (data) => {
17646
+ capturedStdout = appendCapturedSandboxOutput(capturedStdout, data);
17647
+ },
17648
+ onStderr: (data) => {
17649
+ capturedStderr = appendCapturedSandboxOutput(capturedStderr, data);
17612
17650
  }
17613
17651
  });
17614
17652
  const exitCode = raw.result.exit_code ?? raw.result.exitCode ?? 0;
@@ -17625,10 +17663,33 @@ async function runViaSandbox(plan, dependencies) {
17625
17663
  stderr,
17626
17664
  cleanup: raw.cleanup
17627
17665
  };
17666
+ } catch (error) {
17667
+ if (capturedStdout || capturedStderr) {
17668
+ throw buildSandboxStreamError(error, capturedStdout, capturedStderr);
17669
+ }
17670
+ throw error;
17628
17671
  } finally {
17629
17672
  bundle.cleanup?.();
17630
17673
  }
17631
17674
  }
17675
+ function appendCapturedSandboxOutput(current, data) {
17676
+ const next = current + data;
17677
+ if (next.length <= MAX_CAPTURED_SANDBOX_OUTPUT)
17678
+ return next;
17679
+ return next.slice(next.length - MAX_CAPTURED_SANDBOX_OUTPUT);
17680
+ }
17681
+ function buildSandboxStreamError(error, stdout, stderr) {
17682
+ const message = error instanceof Error ? error.message : String(error);
17683
+ const parts = [`Sandbox workflow execution failed: ${message}`];
17684
+ if (stdout.trim())
17685
+ parts.push(`stdout:
17686
+ ${stdout.trimEnd()}`);
17687
+ if (stderr.trim())
17688
+ parts.push(`stderr:
17689
+ ${stderr.trimEnd()}`);
17690
+ return new Error(parts.join(`
17691
+ `));
17692
+ }
17632
17693
  function resolveSandboxEnv(env2) {
17633
17694
  if (!env2 || Object.keys(env2).length === 0)
17634
17695
  return;
@@ -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.59",
55
+ version: "0.0.61",
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",
@@ -23816,18 +23816,46 @@ function buildSandboxCommand(input) {
23816
23816
  "--no-auto-generate",
23817
23817
  "--json"
23818
23818
  ];
23819
+ const installBrowserArgs = [
23820
+ "bunx",
23821
+ input.packageSpec,
23822
+ "install-browser",
23823
+ "--engine",
23824
+ "playwright"
23825
+ ];
23819
23826
  return [
23820
23827
  "set -euo pipefail",
23828
+ buildBunBootstrapCommand(),
23821
23829
  `mkdir -p ${shellQuote(input.remoteDir)}`,
23822
23830
  `mkdir -p ${shellQuote(input.stateRemoteDir)}`,
23823
23831
  input.appRemoteDir ? `mkdir -p ${shellQuote(input.appRemoteDir)}` : undefined,
23824
23832
  `cd ${shellQuote(input.appRemoteDir ?? input.remoteDir)}`,
23825
23833
  input.setupCommand,
23826
23834
  buildAppStartCommand(input),
23835
+ buildSandboxBrowserInstallCommand(installBrowserArgs),
23827
23836
  `HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
23828
23837
  ].filter(Boolean).join(`
23829
23838
  `);
23830
23839
  }
23840
+ function buildBunBootstrapCommand() {
23841
+ return [
23842
+ 'export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"',
23843
+ 'export PATH="$BUN_INSTALL/bin:$PATH"',
23844
+ "if ! command -v bun >/dev/null 2>&1; then",
23845
+ " curl -fsSL https://bun.sh/install | bash",
23846
+ "fi",
23847
+ "command -v bun >/dev/null 2>&1"
23848
+ ].join(`
23849
+ `);
23850
+ }
23851
+ function buildSandboxBrowserInstallCommand(args) {
23852
+ return [
23853
+ 'if [ "${TESTERS_SANDBOX_SKIP_BROWSER_INSTALL:-}" != "1" ]; then',
23854
+ ` ${args.map(shellQuote).join(" ")}`,
23855
+ "fi"
23856
+ ].join(`
23857
+ `);
23858
+ }
23831
23859
  function buildAppStartCommand(input) {
23832
23860
  if (!input.appStartCommand)
23833
23861
  return;
@@ -23865,14 +23893,17 @@ async function runViaSandbox(plan, dependencies) {
23865
23893
  const sandboxes = await resolveSandboxesRuntime(dependencies);
23866
23894
  const createBundle = dependencies.createDatabaseBundle ?? createWorkflowDatabaseBundle;
23867
23895
  const bundle = createBundle(plan.workflow, plan);
23896
+ const sandboxTimeoutSeconds = plan.sandbox.timeoutMs === undefined ? undefined : Math.ceil(plan.sandbox.timeoutMs / 1000);
23897
+ let capturedStdout = "";
23898
+ let capturedStderr = "";
23868
23899
  try {
23869
23900
  const raw = await sandboxes.runCommandInSandbox({
23870
23901
  command: plan.sandbox.command,
23871
23902
  provider: plan.sandbox.provider,
23872
23903
  name: plan.sandbox.name,
23873
23904
  image: plan.sandbox.image,
23874
- sandboxTimeout: plan.sandbox.timeoutMs,
23875
- commandTimeoutMs: plan.sandbox.timeoutMs,
23905
+ sandboxTimeout: sandboxTimeoutSeconds,
23906
+ commandTimeoutMs: sandboxTimeoutSeconds,
23876
23907
  config: {
23877
23908
  source: "testers",
23878
23909
  testersProjectId: plan.workflow.projectId ?? undefined,
@@ -23885,6 +23916,12 @@ async function runViaSandbox(plan, dependencies) {
23885
23916
  localDir: bundle.localDir,
23886
23917
  remoteDir: bundle.remoteDir,
23887
23918
  syncStrategy: plan.sandbox.syncStrategy
23919
+ },
23920
+ onStdout: (data) => {
23921
+ capturedStdout = appendCapturedSandboxOutput(capturedStdout, data);
23922
+ },
23923
+ onStderr: (data) => {
23924
+ capturedStderr = appendCapturedSandboxOutput(capturedStderr, data);
23888
23925
  }
23889
23926
  });
23890
23927
  const exitCode = raw.result.exit_code ?? raw.result.exitCode ?? 0;
@@ -23901,10 +23938,33 @@ async function runViaSandbox(plan, dependencies) {
23901
23938
  stderr,
23902
23939
  cleanup: raw.cleanup
23903
23940
  };
23941
+ } catch (error) {
23942
+ if (capturedStdout || capturedStderr) {
23943
+ throw buildSandboxStreamError(error, capturedStdout, capturedStderr);
23944
+ }
23945
+ throw error;
23904
23946
  } finally {
23905
23947
  bundle.cleanup?.();
23906
23948
  }
23907
23949
  }
23950
+ function appendCapturedSandboxOutput(current, data) {
23951
+ const next = current + data;
23952
+ if (next.length <= MAX_CAPTURED_SANDBOX_OUTPUT)
23953
+ return next;
23954
+ return next.slice(next.length - MAX_CAPTURED_SANDBOX_OUTPUT);
23955
+ }
23956
+ function buildSandboxStreamError(error, stdout, stderr) {
23957
+ const message = error instanceof Error ? error.message : String(error);
23958
+ const parts = [`Sandbox workflow execution failed: ${message}`];
23959
+ if (stdout.trim())
23960
+ parts.push(`stdout:
23961
+ ${stdout.trimEnd()}`);
23962
+ if (stderr.trim())
23963
+ parts.push(`stderr:
23964
+ ${stderr.trimEnd()}`);
23965
+ return new Error(parts.join(`
23966
+ `));
23967
+ }
23908
23968
  function resolveSandboxEnv(env2) {
23909
23969
  if (!env2 || Object.keys(env2).length === 0)
23910
23970
  return;
@@ -23936,7 +23996,7 @@ async function resolveSandboxesRuntime(dependencies) {
23936
23996
  function shellQuote(value) {
23937
23997
  return `'${value.replaceAll("'", `'"'"'`)}'`;
23938
23998
  }
23939
- var APP_SOURCE_EXCLUDES;
23999
+ var APP_SOURCE_EXCLUDES, MAX_CAPTURED_SANDBOX_OUTPUT = 120000;
23940
24000
  var init_workflow_runner = __esm(() => {
23941
24001
  init_database();
23942
24002
  init_workflows();
@@ -47090,7 +47090,7 @@ import { join as join14 } from "path";
47090
47090
  // package.json
47091
47091
  var package_default = {
47092
47092
  name: "@hasna/testers",
47093
- version: "0.0.59",
47093
+ version: "0.0.61",
47094
47094
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
47095
47095
  type: "module",
47096
47096
  main: "dist/index.js",
@@ -51500,6 +51500,7 @@ var APP_SOURCE_EXCLUDES = [
51500
51500
  ".venv",
51501
51501
  "__pycache__"
51502
51502
  ];
51503
+ var MAX_CAPTURED_SANDBOX_OUTPUT = 120000;
51503
51504
  function buildWorkflowRunPlan(workflow, options) {
51504
51505
  const runOptions = {
51505
51506
  url: options.url,
@@ -51656,18 +51657,46 @@ function buildSandboxCommand(input) {
51656
51657
  "--no-auto-generate",
51657
51658
  "--json"
51658
51659
  ];
51660
+ const installBrowserArgs = [
51661
+ "bunx",
51662
+ input.packageSpec,
51663
+ "install-browser",
51664
+ "--engine",
51665
+ "playwright"
51666
+ ];
51659
51667
  return [
51660
51668
  "set -euo pipefail",
51669
+ buildBunBootstrapCommand(),
51661
51670
  `mkdir -p ${shellQuote(input.remoteDir)}`,
51662
51671
  `mkdir -p ${shellQuote(input.stateRemoteDir)}`,
51663
51672
  input.appRemoteDir ? `mkdir -p ${shellQuote(input.appRemoteDir)}` : undefined,
51664
51673
  `cd ${shellQuote(input.appRemoteDir ?? input.remoteDir)}`,
51665
51674
  input.setupCommand,
51666
51675
  buildAppStartCommand(input),
51676
+ buildSandboxBrowserInstallCommand(installBrowserArgs),
51667
51677
  `HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
51668
51678
  ].filter(Boolean).join(`
51669
51679
  `);
51670
51680
  }
51681
+ function buildBunBootstrapCommand() {
51682
+ return [
51683
+ 'export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"',
51684
+ 'export PATH="$BUN_INSTALL/bin:$PATH"',
51685
+ "if ! command -v bun >/dev/null 2>&1; then",
51686
+ " curl -fsSL https://bun.sh/install | bash",
51687
+ "fi",
51688
+ "command -v bun >/dev/null 2>&1"
51689
+ ].join(`
51690
+ `);
51691
+ }
51692
+ function buildSandboxBrowserInstallCommand(args) {
51693
+ return [
51694
+ 'if [ "${TESTERS_SANDBOX_SKIP_BROWSER_INSTALL:-}" != "1" ]; then',
51695
+ ` ${args.map(shellQuote).join(" ")}`,
51696
+ "fi"
51697
+ ].join(`
51698
+ `);
51699
+ }
51671
51700
  function buildAppStartCommand(input) {
51672
51701
  if (!input.appStartCommand)
51673
51702
  return;
@@ -51705,14 +51734,17 @@ async function runViaSandbox(plan, dependencies) {
51705
51734
  const sandboxes = await resolveSandboxesRuntime(dependencies);
51706
51735
  const createBundle = dependencies.createDatabaseBundle ?? createWorkflowDatabaseBundle;
51707
51736
  const bundle = createBundle(plan.workflow, plan);
51737
+ const sandboxTimeoutSeconds = plan.sandbox.timeoutMs === undefined ? undefined : Math.ceil(plan.sandbox.timeoutMs / 1000);
51738
+ let capturedStdout = "";
51739
+ let capturedStderr = "";
51708
51740
  try {
51709
51741
  const raw = await sandboxes.runCommandInSandbox({
51710
51742
  command: plan.sandbox.command,
51711
51743
  provider: plan.sandbox.provider,
51712
51744
  name: plan.sandbox.name,
51713
51745
  image: plan.sandbox.image,
51714
- sandboxTimeout: plan.sandbox.timeoutMs,
51715
- commandTimeoutMs: plan.sandbox.timeoutMs,
51746
+ sandboxTimeout: sandboxTimeoutSeconds,
51747
+ commandTimeoutMs: sandboxTimeoutSeconds,
51716
51748
  config: {
51717
51749
  source: "testers",
51718
51750
  testersProjectId: plan.workflow.projectId ?? undefined,
@@ -51725,6 +51757,12 @@ async function runViaSandbox(plan, dependencies) {
51725
51757
  localDir: bundle.localDir,
51726
51758
  remoteDir: bundle.remoteDir,
51727
51759
  syncStrategy: plan.sandbox.syncStrategy
51760
+ },
51761
+ onStdout: (data) => {
51762
+ capturedStdout = appendCapturedSandboxOutput(capturedStdout, data);
51763
+ },
51764
+ onStderr: (data) => {
51765
+ capturedStderr = appendCapturedSandboxOutput(capturedStderr, data);
51728
51766
  }
51729
51767
  });
51730
51768
  const exitCode = raw.result.exit_code ?? raw.result.exitCode ?? 0;
@@ -51741,10 +51779,33 @@ async function runViaSandbox(plan, dependencies) {
51741
51779
  stderr,
51742
51780
  cleanup: raw.cleanup
51743
51781
  };
51782
+ } catch (error) {
51783
+ if (capturedStdout || capturedStderr) {
51784
+ throw buildSandboxStreamError(error, capturedStdout, capturedStderr);
51785
+ }
51786
+ throw error;
51744
51787
  } finally {
51745
51788
  bundle.cleanup?.();
51746
51789
  }
51747
51790
  }
51791
+ function appendCapturedSandboxOutput(current, data) {
51792
+ const next = current + data;
51793
+ if (next.length <= MAX_CAPTURED_SANDBOX_OUTPUT)
51794
+ return next;
51795
+ return next.slice(next.length - MAX_CAPTURED_SANDBOX_OUTPUT);
51796
+ }
51797
+ function buildSandboxStreamError(error, stdout, stderr) {
51798
+ const message = error instanceof Error ? error.message : String(error);
51799
+ const parts = [`Sandbox workflow execution failed: ${message}`];
51800
+ if (stdout.trim())
51801
+ parts.push(`stdout:
51802
+ ${stdout.trimEnd()}`);
51803
+ if (stderr.trim())
51804
+ parts.push(`stderr:
51805
+ ${stderr.trimEnd()}`);
51806
+ return new Error(parts.join(`
51807
+ `));
51808
+ }
51748
51809
  function resolveSandboxEnv(env) {
51749
51810
  if (!env || Object.keys(env).length === 0)
51750
51811
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/testers",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
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",