@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 +64 -4
- package/dist/index.js +63 -2
- package/dist/lib/workflow-runner.d.ts.map +1 -1
- package/dist/mcp/index.js +64 -4
- package/dist/server/index.js +64 -3
- package/package.json +1 -1
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:
|
|
27484
|
-
commandTimeoutMs:
|
|
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.
|
|
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:
|
|
17599
|
-
commandTimeoutMs:
|
|
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.
|
|
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:
|
|
23875
|
-
commandTimeoutMs:
|
|
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();
|
package/dist/server/index.js
CHANGED
|
@@ -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.
|
|
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:
|
|
51715
|
-
commandTimeoutMs:
|
|
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;
|