@hasna/testers 0.0.44 → 0.0.46
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 +218 -72
- package/dist/index.js +160 -42
- package/dist/lib/workflow-runner.d.ts +6 -0
- package/dist/lib/workflow-runner.d.ts.map +1 -1
- package/dist/mcp/index.js +135 -16
- package/dist/server/index.js +130 -12
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
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.46",
|
|
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",
|
|
@@ -14178,6 +14178,12 @@ function workflowExecutionFromValue(value) {
|
|
|
14178
14178
|
const sandboxSyncStrategy = syncStrategyValue(input["sandboxSyncStrategy"]);
|
|
14179
14179
|
const setupCommand = stringValue(input["setupCommand"]);
|
|
14180
14180
|
const packageSpec = stringValue(input["packageSpec"]);
|
|
14181
|
+
const appSourceDir = stringValue(input["appSourceDir"]);
|
|
14182
|
+
const appRemoteDir = stringValue(input["appRemoteDir"]);
|
|
14183
|
+
const appStartCommand = stringValue(input["appStartCommand"]);
|
|
14184
|
+
const appUrl = stringValue(input["appUrl"]);
|
|
14185
|
+
const appWaitUrl = stringValue(input["appWaitUrl"]);
|
|
14186
|
+
const appWaitTimeoutMs = numberValue(input["appWaitTimeoutMs"]);
|
|
14181
14187
|
const timeoutMs = numberValue(input["timeoutMs"]);
|
|
14182
14188
|
const env = stringMap(input["env"]);
|
|
14183
14189
|
return {
|
|
@@ -14189,6 +14195,12 @@ function workflowExecutionFromValue(value) {
|
|
|
14189
14195
|
...sandboxSyncStrategy ? { sandboxSyncStrategy } : {},
|
|
14190
14196
|
...setupCommand ? { setupCommand } : {},
|
|
14191
14197
|
...packageSpec ? { packageSpec } : {},
|
|
14198
|
+
...appSourceDir ? { appSourceDir } : {},
|
|
14199
|
+
...appRemoteDir ? { appRemoteDir } : {},
|
|
14200
|
+
...appStartCommand ? { appStartCommand } : {},
|
|
14201
|
+
...appUrl ? { appUrl } : {},
|
|
14202
|
+
...appWaitUrl ? { appWaitUrl } : {},
|
|
14203
|
+
...appWaitTimeoutMs !== undefined ? { appWaitTimeoutMs } : {},
|
|
14192
14204
|
...timeoutMs !== undefined ? { timeoutMs } : {},
|
|
14193
14205
|
...env ? { env } : {}
|
|
14194
14206
|
};
|
|
@@ -23488,9 +23500,10 @@ var init_workflows = __esm(() => {
|
|
|
23488
23500
|
});
|
|
23489
23501
|
|
|
23490
23502
|
// src/lib/workflow-runner.ts
|
|
23491
|
-
import {
|
|
23503
|
+
import { spawnSync } from "child_process";
|
|
23504
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync9, mkdtempSync, rmSync, statSync } from "fs";
|
|
23492
23505
|
import { tmpdir } from "os";
|
|
23493
|
-
import { join as join14 } from "path";
|
|
23506
|
+
import { join as join14, posix as pathPosix } from "path";
|
|
23494
23507
|
function buildWorkflowRunPlan(workflow, options) {
|
|
23495
23508
|
const runOptions = {
|
|
23496
23509
|
url: options.url,
|
|
@@ -23532,10 +23545,16 @@ function createWorkflowDatabaseBundle(workflow, plan) {
|
|
|
23532
23545
|
if (!plan.sandbox)
|
|
23533
23546
|
throw new Error(`Workflow is not configured for sandbox execution: ${workflow.name}`);
|
|
23534
23547
|
const localDir = mkdtempSync(join14(tmpdir(), `testers-workflow-${workflow.id.slice(0, 8)}-`));
|
|
23535
|
-
|
|
23548
|
+
const stateDir = join14(localDir, ".testers-state");
|
|
23549
|
+
mkdirSync9(stateDir, { recursive: true });
|
|
23550
|
+
writeDatabaseSnapshot(join14(stateDir, "testers.db"));
|
|
23551
|
+
if (plan.sandbox.appSourceDir && plan.sandbox.appRemoteDir) {
|
|
23552
|
+
const relativeAppDir = relativeRemotePath(plan.sandbox.remoteDir, plan.sandbox.appRemoteDir);
|
|
23553
|
+
copyAppSource(plan.sandbox.appSourceDir, join14(localDir, relativeAppDir));
|
|
23554
|
+
}
|
|
23536
23555
|
return {
|
|
23537
23556
|
localDir,
|
|
23538
|
-
remoteDir: plan.sandbox.
|
|
23557
|
+
remoteDir: plan.sandbox.remoteDir,
|
|
23539
23558
|
cleanup: () => rmSync(localDir, { recursive: true, force: true })
|
|
23540
23559
|
};
|
|
23541
23560
|
}
|
|
@@ -23546,15 +23565,63 @@ function validatePersonaIds(workflow) {
|
|
|
23546
23565
|
}
|
|
23547
23566
|
}
|
|
23548
23567
|
}
|
|
23568
|
+
function relativeRemotePath(remoteDir, remoteChildDir) {
|
|
23569
|
+
if (!remoteChildDir.startsWith("/")) {
|
|
23570
|
+
const relative3 = remoteChildDir.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
23571
|
+
if (!relative3 || relative3 === ".") {
|
|
23572
|
+
throw new Error("Sandbox app remote directory must be a child directory, not the workflow root");
|
|
23573
|
+
}
|
|
23574
|
+
return relative3;
|
|
23575
|
+
}
|
|
23576
|
+
const base = remoteDir.replace(/\/+$/, "") || "/";
|
|
23577
|
+
const child = remoteChildDir.replace(/\/+$/, "") || "/";
|
|
23578
|
+
const relative2 = pathPosix.relative(base, child);
|
|
23579
|
+
if (!relative2 || relative2 === "." || relative2.startsWith("..") || pathPosix.isAbsolute(relative2)) {
|
|
23580
|
+
throw new Error(`Sandbox app remote directory must be inside the workflow remote directory (${remoteDir}): ${remoteChildDir}`);
|
|
23581
|
+
}
|
|
23582
|
+
return relative2;
|
|
23583
|
+
}
|
|
23584
|
+
function copyAppSource(sourceDir, targetDir) {
|
|
23585
|
+
if (!existsSync11(sourceDir) || !statSync(sourceDir).isDirectory()) {
|
|
23586
|
+
throw new Error(`Sandbox app source directory does not exist or is not a directory: ${sourceDir}`);
|
|
23587
|
+
}
|
|
23588
|
+
mkdirSync9(targetDir, { recursive: true });
|
|
23589
|
+
const result = spawnSync("rsync", [
|
|
23590
|
+
"-a",
|
|
23591
|
+
"--delete",
|
|
23592
|
+
...APP_SOURCE_EXCLUDES.flatMap((item) => ["--exclude", item]),
|
|
23593
|
+
`${sourceDir.replace(/\/+$/, "")}/`,
|
|
23594
|
+
`${targetDir.replace(/\/+$/, "")}/`
|
|
23595
|
+
], { encoding: "utf8" });
|
|
23596
|
+
if (result.error) {
|
|
23597
|
+
throw new Error(`Failed to rsync sandbox app source: ${result.error.message}`);
|
|
23598
|
+
}
|
|
23599
|
+
if (result.status !== 0) {
|
|
23600
|
+
throw new Error(`Failed to rsync sandbox app source (${result.status}): ${result.stderr.trim()}`);
|
|
23601
|
+
}
|
|
23602
|
+
}
|
|
23603
|
+
function writeDatabaseSnapshot(targetPath) {
|
|
23604
|
+
getDatabase().exec(`VACUUM INTO ${sqlString(targetPath)}`);
|
|
23605
|
+
}
|
|
23606
|
+
function sqlString(value) {
|
|
23607
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
23608
|
+
}
|
|
23549
23609
|
function buildSandboxPlan(workflow, execution, runOptions) {
|
|
23550
23610
|
const remoteDir = execution.sandboxRemoteDir ?? `/tmp/testers-workflow-${workflow.id.slice(0, 8)}`;
|
|
23551
23611
|
const stateRemoteDir = `${remoteDir.replace(/\/+$/, "")}/.testers-state`;
|
|
23612
|
+
const appRemoteDir = execution.appSourceDir ? execution.appRemoteDir ?? `${remoteDir.replace(/\/+$/, "")}/app` : execution.appRemoteDir;
|
|
23552
23613
|
return {
|
|
23553
23614
|
provider: execution.provider,
|
|
23554
23615
|
image: execution.sandboxImage,
|
|
23555
23616
|
name: `testers-${workflow.id.slice(0, 8)}`,
|
|
23556
23617
|
remoteDir,
|
|
23557
23618
|
stateRemoteDir,
|
|
23619
|
+
...execution.appSourceDir ? { appSourceDir: execution.appSourceDir } : {},
|
|
23620
|
+
...appRemoteDir ? { appRemoteDir } : {},
|
|
23621
|
+
...execution.appStartCommand ? { appStartCommand: execution.appStartCommand } : {},
|
|
23622
|
+
...execution.appUrl ? { appUrl: execution.appUrl } : {},
|
|
23623
|
+
...execution.appWaitUrl ? { appWaitUrl: execution.appWaitUrl } : {},
|
|
23624
|
+
...execution.appWaitTimeoutMs !== undefined ? { appWaitTimeoutMs: execution.appWaitTimeoutMs } : {},
|
|
23558
23625
|
cleanup: execution.sandboxCleanup ?? "delete",
|
|
23559
23626
|
syncStrategy: execution.sandboxSyncStrategy ?? "rsync",
|
|
23560
23627
|
timeoutMs: execution.timeoutMs,
|
|
@@ -23562,6 +23629,12 @@ function buildSandboxPlan(workflow, execution, runOptions) {
|
|
|
23562
23629
|
command: buildSandboxCommand({
|
|
23563
23630
|
runOptions,
|
|
23564
23631
|
remoteDir,
|
|
23632
|
+
stateRemoteDir,
|
|
23633
|
+
appRemoteDir,
|
|
23634
|
+
appStartCommand: execution.appStartCommand,
|
|
23635
|
+
appUrl: execution.appUrl,
|
|
23636
|
+
appWaitUrl: execution.appWaitUrl,
|
|
23637
|
+
appWaitTimeoutMs: execution.appWaitTimeoutMs,
|
|
23565
23638
|
dbPath: `${stateRemoteDir}/testers.db`,
|
|
23566
23639
|
setupCommand: execution.setupCommand,
|
|
23567
23640
|
packageSpec: execution.packageSpec ?? "@hasna/testers"
|
|
@@ -23569,11 +23642,12 @@ function buildSandboxPlan(workflow, execution, runOptions) {
|
|
|
23569
23642
|
};
|
|
23570
23643
|
}
|
|
23571
23644
|
function buildSandboxCommand(input) {
|
|
23645
|
+
const targetUrl = input.appUrl ?? input.runOptions.url;
|
|
23572
23646
|
const args = [
|
|
23573
23647
|
"bunx",
|
|
23574
23648
|
input.packageSpec,
|
|
23575
23649
|
"run",
|
|
23576
|
-
|
|
23650
|
+
targetUrl,
|
|
23577
23651
|
...input.runOptions.scenarioIds?.length ? ["--scenario", input.runOptions.scenarioIds.join(",")] : [],
|
|
23578
23652
|
...input.runOptions.tags?.length ? input.runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
|
|
23579
23653
|
...input.runOptions.priority ? ["--priority", input.runOptions.priority] : [],
|
|
@@ -23589,12 +23663,46 @@ function buildSandboxCommand(input) {
|
|
|
23589
23663
|
return [
|
|
23590
23664
|
"set -euo pipefail",
|
|
23591
23665
|
`mkdir -p ${shellQuote(input.remoteDir)}`,
|
|
23592
|
-
`
|
|
23666
|
+
`mkdir -p ${shellQuote(input.stateRemoteDir)}`,
|
|
23667
|
+
input.appRemoteDir ? `mkdir -p ${shellQuote(input.appRemoteDir)}` : undefined,
|
|
23668
|
+
`cd ${shellQuote(input.appRemoteDir ?? input.remoteDir)}`,
|
|
23593
23669
|
input.setupCommand,
|
|
23670
|
+
buildAppStartCommand(input),
|
|
23594
23671
|
`HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
|
|
23595
23672
|
].filter(Boolean).join(`
|
|
23596
23673
|
`);
|
|
23597
23674
|
}
|
|
23675
|
+
function buildAppStartCommand(input) {
|
|
23676
|
+
if (!input.appStartCommand)
|
|
23677
|
+
return;
|
|
23678
|
+
const waitUrl = input.appWaitUrl ?? input.appUrl ?? input.runOptions.url;
|
|
23679
|
+
const waitTimeoutMs = input.appWaitTimeoutMs ?? 120000;
|
|
23680
|
+
return [
|
|
23681
|
+
`( ${input.appStartCommand} ) > ${shellQuote(`${input.stateRemoteDir}/app.log`)} 2>&1 &`,
|
|
23682
|
+
"APP_PID=$!",
|
|
23683
|
+
`echo "$APP_PID" > ${shellQuote(`${input.stateRemoteDir}/app.pid`)}`,
|
|
23684
|
+
`trap 'kill "$APP_PID" 2>/dev/null || true' EXIT`,
|
|
23685
|
+
waitUrl ? buildWaitForUrlCommand(waitUrl, waitTimeoutMs) : undefined
|
|
23686
|
+
].filter(Boolean).join(`
|
|
23687
|
+
`);
|
|
23688
|
+
}
|
|
23689
|
+
function buildWaitForUrlCommand(url, timeoutMs) {
|
|
23690
|
+
const script = `
|
|
23691
|
+
const url = ${JSON.stringify(url)};
|
|
23692
|
+
const timeoutMs = ${JSON.stringify(timeoutMs)};
|
|
23693
|
+
const deadline = Date.now() + timeoutMs;
|
|
23694
|
+
while (Date.now() <= deadline) {
|
|
23695
|
+
try {
|
|
23696
|
+
const response = await fetch(url);
|
|
23697
|
+
if (response.status >= 200 && response.status < 500) process.exit(0);
|
|
23698
|
+
} catch {}
|
|
23699
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
23700
|
+
}
|
|
23701
|
+
console.error(\`Timed out waiting for \${url} after \${timeoutMs}ms\`);
|
|
23702
|
+
process.exit(1);
|
|
23703
|
+
`.trim();
|
|
23704
|
+
return `bun -e ${shellQuote(script)}`;
|
|
23705
|
+
}
|
|
23598
23706
|
async function runViaSandbox(plan, dependencies) {
|
|
23599
23707
|
if (!plan.sandbox)
|
|
23600
23708
|
throw new Error("Workflow does not have a sandbox plan");
|
|
@@ -23652,11 +23760,22 @@ async function resolveSandboxesRuntime(dependencies) {
|
|
|
23652
23760
|
function shellQuote(value) {
|
|
23653
23761
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
23654
23762
|
}
|
|
23763
|
+
var APP_SOURCE_EXCLUDES;
|
|
23655
23764
|
var init_workflow_runner = __esm(() => {
|
|
23656
23765
|
init_database();
|
|
23657
23766
|
init_workflows();
|
|
23658
23767
|
init_personas();
|
|
23659
23768
|
init_runner();
|
|
23769
|
+
APP_SOURCE_EXCLUDES = [
|
|
23770
|
+
"node_modules",
|
|
23771
|
+
".git",
|
|
23772
|
+
"dist",
|
|
23773
|
+
".next",
|
|
23774
|
+
".turbo",
|
|
23775
|
+
".cache",
|
|
23776
|
+
".venv",
|
|
23777
|
+
"__pycache__"
|
|
23778
|
+
];
|
|
23660
23779
|
});
|
|
23661
23780
|
|
|
23662
23781
|
// node_modules/@ai-sdk/provider/dist/index.mjs
|
|
@@ -53711,10 +53830,10 @@ import { exec } from "child_process";
|
|
|
53711
53830
|
import { promisify } from "util";
|
|
53712
53831
|
import { readFileSync as readFileSync3 } from "fs";
|
|
53713
53832
|
import { webcrypto as crypto2 } from "crypto";
|
|
53714
|
-
import { existsSync as existsSync42, writeFileSync as
|
|
53833
|
+
import { existsSync as existsSync42, writeFileSync as writeFileSync3, readFileSync as readFileSync22, mkdirSync as mkdirSync32 } from "fs";
|
|
53715
53834
|
import { join as join42 } from "path";
|
|
53716
53835
|
import { Database as Database4 } from "bun:sqlite";
|
|
53717
|
-
import { existsSync as
|
|
53836
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync10 } from "fs";
|
|
53718
53837
|
import { dirname as dirname4, join as join15, resolve as resolve2 } from "path";
|
|
53719
53838
|
import { existsSync as existsSync22, writeFileSync as writeFileSync4 } from "fs";
|
|
53720
53839
|
import { join as join22 } from "path";
|
|
@@ -53728,10 +53847,10 @@ import * as zlib from "zlib";
|
|
|
53728
53847
|
import { Readable } from "stream";
|
|
53729
53848
|
import { Writable } from "stream";
|
|
53730
53849
|
import { createHash as createHash2 } from "crypto";
|
|
53731
|
-
import { mkdirSync as mkdirSync4, statSync, writeFileSync as writeFileSync42 } from "fs";
|
|
53850
|
+
import { mkdirSync as mkdirSync4, statSync as statSync2, writeFileSync as writeFileSync42 } from "fs";
|
|
53732
53851
|
import { dirname as dirname42, join as join62, relative as relative2 } from "path";
|
|
53733
53852
|
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
53734
|
-
import { existsSync as existsSync62, readdirSync as readdirSync3, readFileSync as readFileSync42, statSync as
|
|
53853
|
+
import { existsSync as existsSync62, readdirSync as readdirSync3, readFileSync as readFileSync42, statSync as statSync22 } from "fs";
|
|
53735
53854
|
import { basename, join as join72, resolve as resolve22 } from "path";
|
|
53736
53855
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
53737
53856
|
import { existsSync as existsSync72, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync52 } from "fs";
|
|
@@ -53896,8 +54015,8 @@ function ensureDir2(filePath) {
|
|
|
53896
54015
|
if (filePath === ":memory:")
|
|
53897
54016
|
return;
|
|
53898
54017
|
const dir = dirname4(resolve2(filePath));
|
|
53899
|
-
if (!
|
|
53900
|
-
|
|
54018
|
+
if (!existsSync12(dir)) {
|
|
54019
|
+
mkdirSync10(dir, { recursive: true });
|
|
53901
54020
|
}
|
|
53902
54021
|
}
|
|
53903
54022
|
function getDatabase2(path) {
|
|
@@ -54178,7 +54297,7 @@ function setIntegrations(id, integrations, db2) {
|
|
|
54178
54297
|
const jsonPath = join42(project.path, ".project.json");
|
|
54179
54298
|
if (existsSync42(jsonPath)) {
|
|
54180
54299
|
const existing = JSON.parse(readFileSync22(jsonPath, "utf-8"));
|
|
54181
|
-
|
|
54300
|
+
writeFileSync3(jsonPath, JSON.stringify({ ...existing, integrations: merged }, null, 2) + `
|
|
54182
54301
|
`, "utf-8");
|
|
54183
54302
|
}
|
|
54184
54303
|
} catch {}
|
|
@@ -55256,7 +55375,7 @@ async function collectLocalFiles(rootPath) {
|
|
|
55256
55375
|
if (entry.isDirectory()) {
|
|
55257
55376
|
await walk(fullPath);
|
|
55258
55377
|
} else if (entry.isFile()) {
|
|
55259
|
-
const stat =
|
|
55378
|
+
const stat = statSync2(fullPath);
|
|
55260
55379
|
if (stat.size > MAX_FILE_SIZE)
|
|
55261
55380
|
continue;
|
|
55262
55381
|
const relPath = relative2(rootPath, fullPath);
|
|
@@ -55421,7 +55540,7 @@ async function importProject(projectPath, options = {}) {
|
|
|
55421
55540
|
if (!existsSync62(absPath)) {
|
|
55422
55541
|
return { error: `Path does not exist: ${absPath}` };
|
|
55423
55542
|
}
|
|
55424
|
-
const stat =
|
|
55543
|
+
const stat = statSync22(absPath);
|
|
55425
55544
|
if (!stat.isDirectory()) {
|
|
55426
55545
|
return { error: `Not a directory: ${absPath}` };
|
|
55427
55546
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -4078,6 +4078,12 @@ function workflowExecutionFromValue(value) {
|
|
|
4078
4078
|
const sandboxSyncStrategy = syncStrategyValue(input["sandboxSyncStrategy"]);
|
|
4079
4079
|
const setupCommand = stringValue(input["setupCommand"]);
|
|
4080
4080
|
const packageSpec = stringValue(input["packageSpec"]);
|
|
4081
|
+
const appSourceDir = stringValue(input["appSourceDir"]);
|
|
4082
|
+
const appRemoteDir = stringValue(input["appRemoteDir"]);
|
|
4083
|
+
const appStartCommand = stringValue(input["appStartCommand"]);
|
|
4084
|
+
const appUrl = stringValue(input["appUrl"]);
|
|
4085
|
+
const appWaitUrl = stringValue(input["appWaitUrl"]);
|
|
4086
|
+
const appWaitTimeoutMs = numberValue(input["appWaitTimeoutMs"]);
|
|
4081
4087
|
const timeoutMs = numberValue(input["timeoutMs"]);
|
|
4082
4088
|
const env = stringMap(input["env"]);
|
|
4083
4089
|
return {
|
|
@@ -4089,6 +4095,12 @@ function workflowExecutionFromValue(value) {
|
|
|
4089
4095
|
...sandboxSyncStrategy ? { sandboxSyncStrategy } : {},
|
|
4090
4096
|
...setupCommand ? { setupCommand } : {},
|
|
4091
4097
|
...packageSpec ? { packageSpec } : {},
|
|
4098
|
+
...appSourceDir ? { appSourceDir } : {},
|
|
4099
|
+
...appRemoteDir ? { appRemoteDir } : {},
|
|
4100
|
+
...appStartCommand ? { appStartCommand } : {},
|
|
4101
|
+
...appUrl ? { appUrl } : {},
|
|
4102
|
+
...appWaitUrl ? { appWaitUrl } : {},
|
|
4103
|
+
...appWaitTimeoutMs !== undefined ? { appWaitTimeoutMs } : {},
|
|
4092
4104
|
...timeoutMs !== undefined ? { timeoutMs } : {},
|
|
4093
4105
|
...env ? { env } : {}
|
|
4094
4106
|
};
|
|
@@ -46920,12 +46932,12 @@ var init_scan_issues = __esm(() => {
|
|
|
46920
46932
|
});
|
|
46921
46933
|
|
|
46922
46934
|
// src/server/index.ts
|
|
46923
|
-
import { existsSync as
|
|
46935
|
+
import { existsSync as existsSync11 } from "fs";
|
|
46924
46936
|
import { join as join14 } from "path";
|
|
46925
46937
|
// package.json
|
|
46926
46938
|
var package_default = {
|
|
46927
46939
|
name: "@hasna/testers",
|
|
46928
|
-
version: "0.0.
|
|
46940
|
+
version: "0.0.46",
|
|
46929
46941
|
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
46930
46942
|
type: "module",
|
|
46931
46943
|
main: "dist/index.js",
|
|
@@ -51316,9 +51328,20 @@ function db2() {
|
|
|
51316
51328
|
|
|
51317
51329
|
// src/lib/workflow-runner.ts
|
|
51318
51330
|
init_database();
|
|
51319
|
-
import {
|
|
51331
|
+
import { spawnSync } from "child_process";
|
|
51332
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, rmSync, statSync } from "fs";
|
|
51320
51333
|
import { tmpdir } from "os";
|
|
51321
|
-
import { join as join13 } from "path";
|
|
51334
|
+
import { join as join13, posix as pathPosix } from "path";
|
|
51335
|
+
var APP_SOURCE_EXCLUDES = [
|
|
51336
|
+
"node_modules",
|
|
51337
|
+
".git",
|
|
51338
|
+
"dist",
|
|
51339
|
+
".next",
|
|
51340
|
+
".turbo",
|
|
51341
|
+
".cache",
|
|
51342
|
+
".venv",
|
|
51343
|
+
"__pycache__"
|
|
51344
|
+
];
|
|
51322
51345
|
function buildWorkflowRunPlan(workflow, options) {
|
|
51323
51346
|
const runOptions = {
|
|
51324
51347
|
url: options.url,
|
|
@@ -51360,10 +51383,16 @@ function createWorkflowDatabaseBundle(workflow, plan) {
|
|
|
51360
51383
|
if (!plan.sandbox)
|
|
51361
51384
|
throw new Error(`Workflow is not configured for sandbox execution: ${workflow.name}`);
|
|
51362
51385
|
const localDir = mkdtempSync(join13(tmpdir(), `testers-workflow-${workflow.id.slice(0, 8)}-`));
|
|
51363
|
-
|
|
51386
|
+
const stateDir = join13(localDir, ".testers-state");
|
|
51387
|
+
mkdirSync7(stateDir, { recursive: true });
|
|
51388
|
+
writeDatabaseSnapshot(join13(stateDir, "testers.db"));
|
|
51389
|
+
if (plan.sandbox.appSourceDir && plan.sandbox.appRemoteDir) {
|
|
51390
|
+
const relativeAppDir = relativeRemotePath(plan.sandbox.remoteDir, plan.sandbox.appRemoteDir);
|
|
51391
|
+
copyAppSource(plan.sandbox.appSourceDir, join13(localDir, relativeAppDir));
|
|
51392
|
+
}
|
|
51364
51393
|
return {
|
|
51365
51394
|
localDir,
|
|
51366
|
-
remoteDir: plan.sandbox.
|
|
51395
|
+
remoteDir: plan.sandbox.remoteDir,
|
|
51367
51396
|
cleanup: () => rmSync(localDir, { recursive: true, force: true })
|
|
51368
51397
|
};
|
|
51369
51398
|
}
|
|
@@ -51374,15 +51403,63 @@ function validatePersonaIds(workflow) {
|
|
|
51374
51403
|
}
|
|
51375
51404
|
}
|
|
51376
51405
|
}
|
|
51406
|
+
function relativeRemotePath(remoteDir, remoteChildDir) {
|
|
51407
|
+
if (!remoteChildDir.startsWith("/")) {
|
|
51408
|
+
const relative3 = remoteChildDir.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
51409
|
+
if (!relative3 || relative3 === ".") {
|
|
51410
|
+
throw new Error("Sandbox app remote directory must be a child directory, not the workflow root");
|
|
51411
|
+
}
|
|
51412
|
+
return relative3;
|
|
51413
|
+
}
|
|
51414
|
+
const base = remoteDir.replace(/\/+$/, "") || "/";
|
|
51415
|
+
const child = remoteChildDir.replace(/\/+$/, "") || "/";
|
|
51416
|
+
const relative2 = pathPosix.relative(base, child);
|
|
51417
|
+
if (!relative2 || relative2 === "." || relative2.startsWith("..") || pathPosix.isAbsolute(relative2)) {
|
|
51418
|
+
throw new Error(`Sandbox app remote directory must be inside the workflow remote directory (${remoteDir}): ${remoteChildDir}`);
|
|
51419
|
+
}
|
|
51420
|
+
return relative2;
|
|
51421
|
+
}
|
|
51422
|
+
function copyAppSource(sourceDir, targetDir) {
|
|
51423
|
+
if (!existsSync10(sourceDir) || !statSync(sourceDir).isDirectory()) {
|
|
51424
|
+
throw new Error(`Sandbox app source directory does not exist or is not a directory: ${sourceDir}`);
|
|
51425
|
+
}
|
|
51426
|
+
mkdirSync7(targetDir, { recursive: true });
|
|
51427
|
+
const result = spawnSync("rsync", [
|
|
51428
|
+
"-a",
|
|
51429
|
+
"--delete",
|
|
51430
|
+
...APP_SOURCE_EXCLUDES.flatMap((item) => ["--exclude", item]),
|
|
51431
|
+
`${sourceDir.replace(/\/+$/, "")}/`,
|
|
51432
|
+
`${targetDir.replace(/\/+$/, "")}/`
|
|
51433
|
+
], { encoding: "utf8" });
|
|
51434
|
+
if (result.error) {
|
|
51435
|
+
throw new Error(`Failed to rsync sandbox app source: ${result.error.message}`);
|
|
51436
|
+
}
|
|
51437
|
+
if (result.status !== 0) {
|
|
51438
|
+
throw new Error(`Failed to rsync sandbox app source (${result.status}): ${result.stderr.trim()}`);
|
|
51439
|
+
}
|
|
51440
|
+
}
|
|
51441
|
+
function writeDatabaseSnapshot(targetPath) {
|
|
51442
|
+
getDatabase().exec(`VACUUM INTO ${sqlString(targetPath)}`);
|
|
51443
|
+
}
|
|
51444
|
+
function sqlString(value) {
|
|
51445
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
51446
|
+
}
|
|
51377
51447
|
function buildSandboxPlan(workflow, execution, runOptions) {
|
|
51378
51448
|
const remoteDir = execution.sandboxRemoteDir ?? `/tmp/testers-workflow-${workflow.id.slice(0, 8)}`;
|
|
51379
51449
|
const stateRemoteDir = `${remoteDir.replace(/\/+$/, "")}/.testers-state`;
|
|
51450
|
+
const appRemoteDir = execution.appSourceDir ? execution.appRemoteDir ?? `${remoteDir.replace(/\/+$/, "")}/app` : execution.appRemoteDir;
|
|
51380
51451
|
return {
|
|
51381
51452
|
provider: execution.provider,
|
|
51382
51453
|
image: execution.sandboxImage,
|
|
51383
51454
|
name: `testers-${workflow.id.slice(0, 8)}`,
|
|
51384
51455
|
remoteDir,
|
|
51385
51456
|
stateRemoteDir,
|
|
51457
|
+
...execution.appSourceDir ? { appSourceDir: execution.appSourceDir } : {},
|
|
51458
|
+
...appRemoteDir ? { appRemoteDir } : {},
|
|
51459
|
+
...execution.appStartCommand ? { appStartCommand: execution.appStartCommand } : {},
|
|
51460
|
+
...execution.appUrl ? { appUrl: execution.appUrl } : {},
|
|
51461
|
+
...execution.appWaitUrl ? { appWaitUrl: execution.appWaitUrl } : {},
|
|
51462
|
+
...execution.appWaitTimeoutMs !== undefined ? { appWaitTimeoutMs: execution.appWaitTimeoutMs } : {},
|
|
51386
51463
|
cleanup: execution.sandboxCleanup ?? "delete",
|
|
51387
51464
|
syncStrategy: execution.sandboxSyncStrategy ?? "rsync",
|
|
51388
51465
|
timeoutMs: execution.timeoutMs,
|
|
@@ -51390,6 +51467,12 @@ function buildSandboxPlan(workflow, execution, runOptions) {
|
|
|
51390
51467
|
command: buildSandboxCommand({
|
|
51391
51468
|
runOptions,
|
|
51392
51469
|
remoteDir,
|
|
51470
|
+
stateRemoteDir,
|
|
51471
|
+
appRemoteDir,
|
|
51472
|
+
appStartCommand: execution.appStartCommand,
|
|
51473
|
+
appUrl: execution.appUrl,
|
|
51474
|
+
appWaitUrl: execution.appWaitUrl,
|
|
51475
|
+
appWaitTimeoutMs: execution.appWaitTimeoutMs,
|
|
51393
51476
|
dbPath: `${stateRemoteDir}/testers.db`,
|
|
51394
51477
|
setupCommand: execution.setupCommand,
|
|
51395
51478
|
packageSpec: execution.packageSpec ?? "@hasna/testers"
|
|
@@ -51397,11 +51480,12 @@ function buildSandboxPlan(workflow, execution, runOptions) {
|
|
|
51397
51480
|
};
|
|
51398
51481
|
}
|
|
51399
51482
|
function buildSandboxCommand(input) {
|
|
51483
|
+
const targetUrl = input.appUrl ?? input.runOptions.url;
|
|
51400
51484
|
const args = [
|
|
51401
51485
|
"bunx",
|
|
51402
51486
|
input.packageSpec,
|
|
51403
51487
|
"run",
|
|
51404
|
-
|
|
51488
|
+
targetUrl,
|
|
51405
51489
|
...input.runOptions.scenarioIds?.length ? ["--scenario", input.runOptions.scenarioIds.join(",")] : [],
|
|
51406
51490
|
...input.runOptions.tags?.length ? input.runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
|
|
51407
51491
|
...input.runOptions.priority ? ["--priority", input.runOptions.priority] : [],
|
|
@@ -51417,12 +51501,46 @@ function buildSandboxCommand(input) {
|
|
|
51417
51501
|
return [
|
|
51418
51502
|
"set -euo pipefail",
|
|
51419
51503
|
`mkdir -p ${shellQuote(input.remoteDir)}`,
|
|
51420
|
-
`
|
|
51504
|
+
`mkdir -p ${shellQuote(input.stateRemoteDir)}`,
|
|
51505
|
+
input.appRemoteDir ? `mkdir -p ${shellQuote(input.appRemoteDir)}` : undefined,
|
|
51506
|
+
`cd ${shellQuote(input.appRemoteDir ?? input.remoteDir)}`,
|
|
51421
51507
|
input.setupCommand,
|
|
51508
|
+
buildAppStartCommand(input),
|
|
51422
51509
|
`HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
|
|
51423
51510
|
].filter(Boolean).join(`
|
|
51424
51511
|
`);
|
|
51425
51512
|
}
|
|
51513
|
+
function buildAppStartCommand(input) {
|
|
51514
|
+
if (!input.appStartCommand)
|
|
51515
|
+
return;
|
|
51516
|
+
const waitUrl = input.appWaitUrl ?? input.appUrl ?? input.runOptions.url;
|
|
51517
|
+
const waitTimeoutMs = input.appWaitTimeoutMs ?? 120000;
|
|
51518
|
+
return [
|
|
51519
|
+
`( ${input.appStartCommand} ) > ${shellQuote(`${input.stateRemoteDir}/app.log`)} 2>&1 &`,
|
|
51520
|
+
"APP_PID=$!",
|
|
51521
|
+
`echo "$APP_PID" > ${shellQuote(`${input.stateRemoteDir}/app.pid`)}`,
|
|
51522
|
+
`trap 'kill "$APP_PID" 2>/dev/null || true' EXIT`,
|
|
51523
|
+
waitUrl ? buildWaitForUrlCommand(waitUrl, waitTimeoutMs) : undefined
|
|
51524
|
+
].filter(Boolean).join(`
|
|
51525
|
+
`);
|
|
51526
|
+
}
|
|
51527
|
+
function buildWaitForUrlCommand(url, timeoutMs) {
|
|
51528
|
+
const script = `
|
|
51529
|
+
const url = ${JSON.stringify(url)};
|
|
51530
|
+
const timeoutMs = ${JSON.stringify(timeoutMs)};
|
|
51531
|
+
const deadline = Date.now() + timeoutMs;
|
|
51532
|
+
while (Date.now() <= deadline) {
|
|
51533
|
+
try {
|
|
51534
|
+
const response = await fetch(url);
|
|
51535
|
+
if (response.status >= 200 && response.status < 500) process.exit(0);
|
|
51536
|
+
} catch {}
|
|
51537
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
51538
|
+
}
|
|
51539
|
+
console.error(\`Timed out waiting for \${url} after \${timeoutMs}ms\`);
|
|
51540
|
+
process.exit(1);
|
|
51541
|
+
`.trim();
|
|
51542
|
+
return `bun -e ${shellQuote(script)}`;
|
|
51543
|
+
}
|
|
51426
51544
|
async function runViaSandbox(plan, dependencies) {
|
|
51427
51545
|
if (!plan.sandbox)
|
|
51428
51546
|
throw new Error("Workflow does not have a sandbox plan");
|
|
@@ -52203,7 +52321,7 @@ async function handleRequest(req) {
|
|
|
52203
52321
|
const screenshot = getScreenshot(id);
|
|
52204
52322
|
if (!screenshot)
|
|
52205
52323
|
return errorResponse("Screenshot not found", 404);
|
|
52206
|
-
if (!
|
|
52324
|
+
if (!existsSync11(screenshot.filePath)) {
|
|
52207
52325
|
return errorResponse("Screenshot file not found on disk", 404);
|
|
52208
52326
|
}
|
|
52209
52327
|
const file2 = Bun.file(screenshot.filePath);
|
|
@@ -52652,7 +52770,7 @@ async function handleRequest(req) {
|
|
|
52652
52770
|
}
|
|
52653
52771
|
if (!pathname.startsWith("/api")) {
|
|
52654
52772
|
const dashboardDir = join14(import.meta.dir, "..", "..", "dashboard", "dist");
|
|
52655
|
-
if (!
|
|
52773
|
+
if (!existsSync11(dashboardDir)) {
|
|
52656
52774
|
return new Response(`<!DOCTYPE html>
|
|
52657
52775
|
<html>
|
|
52658
52776
|
<head><title>Open Testers</title></head>
|
|
@@ -52671,7 +52789,7 @@ async function handleRequest(req) {
|
|
|
52671
52789
|
});
|
|
52672
52790
|
}
|
|
52673
52791
|
const filePath = join14(dashboardDir, pathname === "/" ? "index.html" : pathname);
|
|
52674
|
-
if (
|
|
52792
|
+
if (existsSync11(filePath)) {
|
|
52675
52793
|
const file2 = Bun.file(filePath);
|
|
52676
52794
|
return new Response(file2, {
|
|
52677
52795
|
headers: {
|
|
@@ -52681,7 +52799,7 @@ async function handleRequest(req) {
|
|
|
52681
52799
|
});
|
|
52682
52800
|
}
|
|
52683
52801
|
const indexPath = join14(dashboardDir, "index.html");
|
|
52684
|
-
if (
|
|
52802
|
+
if (existsSync11(indexPath)) {
|
|
52685
52803
|
const file2 = Bun.file(indexPath);
|
|
52686
52804
|
return new Response(file2, {
|
|
52687
52805
|
headers: {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -397,6 +397,12 @@ export interface WorkflowExecutionConfig {
|
|
|
397
397
|
sandboxSyncStrategy?: WorkflowSandboxSyncStrategy;
|
|
398
398
|
setupCommand?: string;
|
|
399
399
|
packageSpec?: string;
|
|
400
|
+
appSourceDir?: string;
|
|
401
|
+
appRemoteDir?: string;
|
|
402
|
+
appStartCommand?: string;
|
|
403
|
+
appUrl?: string;
|
|
404
|
+
appWaitUrl?: string;
|
|
405
|
+
appWaitTimeoutMs?: number;
|
|
400
406
|
timeoutMs?: number;
|
|
401
407
|
env?: Record<string, string>;
|
|
402
408
|
}
|
|
@@ -412,6 +418,12 @@ export interface WorkflowExecutionInput {
|
|
|
412
418
|
sandboxSyncStrategy?: WorkflowSandboxSyncStrategy;
|
|
413
419
|
setupCommand?: string;
|
|
414
420
|
packageSpec?: string;
|
|
421
|
+
appSourceDir?: string;
|
|
422
|
+
appRemoteDir?: string;
|
|
423
|
+
appStartCommand?: string;
|
|
424
|
+
appUrl?: string;
|
|
425
|
+
appWaitUrl?: string;
|
|
426
|
+
appWaitTimeoutMs?: number;
|
|
415
427
|
timeoutMs?: number;
|
|
416
428
|
env?: Record<string, string>;
|
|
417
429
|
}
|