@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/cli/index.js
CHANGED
|
@@ -2142,6 +2142,12 @@ function workflowExecutionFromValue(value) {
|
|
|
2142
2142
|
const sandboxSyncStrategy = syncStrategyValue(input["sandboxSyncStrategy"]);
|
|
2143
2143
|
const setupCommand = stringValue(input["setupCommand"]);
|
|
2144
2144
|
const packageSpec = stringValue(input["packageSpec"]);
|
|
2145
|
+
const appSourceDir = stringValue(input["appSourceDir"]);
|
|
2146
|
+
const appRemoteDir = stringValue(input["appRemoteDir"]);
|
|
2147
|
+
const appStartCommand = stringValue(input["appStartCommand"]);
|
|
2148
|
+
const appUrl = stringValue(input["appUrl"]);
|
|
2149
|
+
const appWaitUrl = stringValue(input["appWaitUrl"]);
|
|
2150
|
+
const appWaitTimeoutMs = numberValue(input["appWaitTimeoutMs"]);
|
|
2145
2151
|
const timeoutMs = numberValue(input["timeoutMs"]);
|
|
2146
2152
|
const env = stringMap(input["env"]);
|
|
2147
2153
|
return {
|
|
@@ -2153,6 +2159,12 @@ function workflowExecutionFromValue(value) {
|
|
|
2153
2159
|
...sandboxSyncStrategy ? { sandboxSyncStrategy } : {},
|
|
2154
2160
|
...setupCommand ? { setupCommand } : {},
|
|
2155
2161
|
...packageSpec ? { packageSpec } : {},
|
|
2162
|
+
...appSourceDir ? { appSourceDir } : {},
|
|
2163
|
+
...appRemoteDir ? { appRemoteDir } : {},
|
|
2164
|
+
...appStartCommand ? { appStartCommand } : {},
|
|
2165
|
+
...appUrl ? { appUrl } : {},
|
|
2166
|
+
...appWaitUrl ? { appWaitUrl } : {},
|
|
2167
|
+
...appWaitTimeoutMs !== undefined ? { appWaitTimeoutMs } : {},
|
|
2156
2168
|
...timeoutMs !== undefined ? { timeoutMs } : {},
|
|
2157
2169
|
...env ? { env } : {}
|
|
2158
2170
|
};
|
|
@@ -27027,9 +27039,10 @@ var init_workflows = __esm(() => {
|
|
|
27027
27039
|
});
|
|
27028
27040
|
|
|
27029
27041
|
// src/lib/workflow-runner.ts
|
|
27030
|
-
import {
|
|
27042
|
+
import { spawnSync } from "child_process";
|
|
27043
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync11, mkdtempSync, rmSync, statSync } from "fs";
|
|
27031
27044
|
import { tmpdir } from "os";
|
|
27032
|
-
import { join as join16 } from "path";
|
|
27045
|
+
import { join as join16, posix as pathPosix } from "path";
|
|
27033
27046
|
function buildWorkflowRunPlan(workflow, options) {
|
|
27034
27047
|
const runOptions = {
|
|
27035
27048
|
url: options.url,
|
|
@@ -27071,10 +27084,16 @@ function createWorkflowDatabaseBundle(workflow, plan) {
|
|
|
27071
27084
|
if (!plan.sandbox)
|
|
27072
27085
|
throw new Error(`Workflow is not configured for sandbox execution: ${workflow.name}`);
|
|
27073
27086
|
const localDir = mkdtempSync(join16(tmpdir(), `testers-workflow-${workflow.id.slice(0, 8)}-`));
|
|
27074
|
-
|
|
27087
|
+
const stateDir = join16(localDir, ".testers-state");
|
|
27088
|
+
mkdirSync11(stateDir, { recursive: true });
|
|
27089
|
+
writeDatabaseSnapshot(join16(stateDir, "testers.db"));
|
|
27090
|
+
if (plan.sandbox.appSourceDir && plan.sandbox.appRemoteDir) {
|
|
27091
|
+
const relativeAppDir = relativeRemotePath(plan.sandbox.remoteDir, plan.sandbox.appRemoteDir);
|
|
27092
|
+
copyAppSource(plan.sandbox.appSourceDir, join16(localDir, relativeAppDir));
|
|
27093
|
+
}
|
|
27075
27094
|
return {
|
|
27076
27095
|
localDir,
|
|
27077
|
-
remoteDir: plan.sandbox.
|
|
27096
|
+
remoteDir: plan.sandbox.remoteDir,
|
|
27078
27097
|
cleanup: () => rmSync(localDir, { recursive: true, force: true })
|
|
27079
27098
|
};
|
|
27080
27099
|
}
|
|
@@ -27085,15 +27104,63 @@ function validatePersonaIds(workflow) {
|
|
|
27085
27104
|
}
|
|
27086
27105
|
}
|
|
27087
27106
|
}
|
|
27107
|
+
function relativeRemotePath(remoteDir, remoteChildDir) {
|
|
27108
|
+
if (!remoteChildDir.startsWith("/")) {
|
|
27109
|
+
const relative3 = remoteChildDir.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
27110
|
+
if (!relative3 || relative3 === ".") {
|
|
27111
|
+
throw new Error("Sandbox app remote directory must be a child directory, not the workflow root");
|
|
27112
|
+
}
|
|
27113
|
+
return relative3;
|
|
27114
|
+
}
|
|
27115
|
+
const base = remoteDir.replace(/\/+$/, "") || "/";
|
|
27116
|
+
const child = remoteChildDir.replace(/\/+$/, "") || "/";
|
|
27117
|
+
const relative2 = pathPosix.relative(base, child);
|
|
27118
|
+
if (!relative2 || relative2 === "." || relative2.startsWith("..") || pathPosix.isAbsolute(relative2)) {
|
|
27119
|
+
throw new Error(`Sandbox app remote directory must be inside the workflow remote directory (${remoteDir}): ${remoteChildDir}`);
|
|
27120
|
+
}
|
|
27121
|
+
return relative2;
|
|
27122
|
+
}
|
|
27123
|
+
function copyAppSource(sourceDir, targetDir) {
|
|
27124
|
+
if (!existsSync14(sourceDir) || !statSync(sourceDir).isDirectory()) {
|
|
27125
|
+
throw new Error(`Sandbox app source directory does not exist or is not a directory: ${sourceDir}`);
|
|
27126
|
+
}
|
|
27127
|
+
mkdirSync11(targetDir, { recursive: true });
|
|
27128
|
+
const result = spawnSync("rsync", [
|
|
27129
|
+
"-a",
|
|
27130
|
+
"--delete",
|
|
27131
|
+
...APP_SOURCE_EXCLUDES.flatMap((item) => ["--exclude", item]),
|
|
27132
|
+
`${sourceDir.replace(/\/+$/, "")}/`,
|
|
27133
|
+
`${targetDir.replace(/\/+$/, "")}/`
|
|
27134
|
+
], { encoding: "utf8" });
|
|
27135
|
+
if (result.error) {
|
|
27136
|
+
throw new Error(`Failed to rsync sandbox app source: ${result.error.message}`);
|
|
27137
|
+
}
|
|
27138
|
+
if (result.status !== 0) {
|
|
27139
|
+
throw new Error(`Failed to rsync sandbox app source (${result.status}): ${result.stderr.trim()}`);
|
|
27140
|
+
}
|
|
27141
|
+
}
|
|
27142
|
+
function writeDatabaseSnapshot(targetPath) {
|
|
27143
|
+
getDatabase().exec(`VACUUM INTO ${sqlString(targetPath)}`);
|
|
27144
|
+
}
|
|
27145
|
+
function sqlString(value) {
|
|
27146
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
27147
|
+
}
|
|
27088
27148
|
function buildSandboxPlan(workflow, execution, runOptions) {
|
|
27089
27149
|
const remoteDir = execution.sandboxRemoteDir ?? `/tmp/testers-workflow-${workflow.id.slice(0, 8)}`;
|
|
27090
27150
|
const stateRemoteDir = `${remoteDir.replace(/\/+$/, "")}/.testers-state`;
|
|
27151
|
+
const appRemoteDir = execution.appSourceDir ? execution.appRemoteDir ?? `${remoteDir.replace(/\/+$/, "")}/app` : execution.appRemoteDir;
|
|
27091
27152
|
return {
|
|
27092
27153
|
provider: execution.provider,
|
|
27093
27154
|
image: execution.sandboxImage,
|
|
27094
27155
|
name: `testers-${workflow.id.slice(0, 8)}`,
|
|
27095
27156
|
remoteDir,
|
|
27096
27157
|
stateRemoteDir,
|
|
27158
|
+
...execution.appSourceDir ? { appSourceDir: execution.appSourceDir } : {},
|
|
27159
|
+
...appRemoteDir ? { appRemoteDir } : {},
|
|
27160
|
+
...execution.appStartCommand ? { appStartCommand: execution.appStartCommand } : {},
|
|
27161
|
+
...execution.appUrl ? { appUrl: execution.appUrl } : {},
|
|
27162
|
+
...execution.appWaitUrl ? { appWaitUrl: execution.appWaitUrl } : {},
|
|
27163
|
+
...execution.appWaitTimeoutMs !== undefined ? { appWaitTimeoutMs: execution.appWaitTimeoutMs } : {},
|
|
27097
27164
|
cleanup: execution.sandboxCleanup ?? "delete",
|
|
27098
27165
|
syncStrategy: execution.sandboxSyncStrategy ?? "rsync",
|
|
27099
27166
|
timeoutMs: execution.timeoutMs,
|
|
@@ -27101,6 +27168,12 @@ function buildSandboxPlan(workflow, execution, runOptions) {
|
|
|
27101
27168
|
command: buildSandboxCommand({
|
|
27102
27169
|
runOptions,
|
|
27103
27170
|
remoteDir,
|
|
27171
|
+
stateRemoteDir,
|
|
27172
|
+
appRemoteDir,
|
|
27173
|
+
appStartCommand: execution.appStartCommand,
|
|
27174
|
+
appUrl: execution.appUrl,
|
|
27175
|
+
appWaitUrl: execution.appWaitUrl,
|
|
27176
|
+
appWaitTimeoutMs: execution.appWaitTimeoutMs,
|
|
27104
27177
|
dbPath: `${stateRemoteDir}/testers.db`,
|
|
27105
27178
|
setupCommand: execution.setupCommand,
|
|
27106
27179
|
packageSpec: execution.packageSpec ?? "@hasna/testers"
|
|
@@ -27108,11 +27181,12 @@ function buildSandboxPlan(workflow, execution, runOptions) {
|
|
|
27108
27181
|
};
|
|
27109
27182
|
}
|
|
27110
27183
|
function buildSandboxCommand(input) {
|
|
27184
|
+
const targetUrl = input.appUrl ?? input.runOptions.url;
|
|
27111
27185
|
const args = [
|
|
27112
27186
|
"bunx",
|
|
27113
27187
|
input.packageSpec,
|
|
27114
27188
|
"run",
|
|
27115
|
-
|
|
27189
|
+
targetUrl,
|
|
27116
27190
|
...input.runOptions.scenarioIds?.length ? ["--scenario", input.runOptions.scenarioIds.join(",")] : [],
|
|
27117
27191
|
...input.runOptions.tags?.length ? input.runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
|
|
27118
27192
|
...input.runOptions.priority ? ["--priority", input.runOptions.priority] : [],
|
|
@@ -27128,12 +27202,46 @@ function buildSandboxCommand(input) {
|
|
|
27128
27202
|
return [
|
|
27129
27203
|
"set -euo pipefail",
|
|
27130
27204
|
`mkdir -p ${shellQuote(input.remoteDir)}`,
|
|
27131
|
-
`
|
|
27205
|
+
`mkdir -p ${shellQuote(input.stateRemoteDir)}`,
|
|
27206
|
+
input.appRemoteDir ? `mkdir -p ${shellQuote(input.appRemoteDir)}` : undefined,
|
|
27207
|
+
`cd ${shellQuote(input.appRemoteDir ?? input.remoteDir)}`,
|
|
27132
27208
|
input.setupCommand,
|
|
27209
|
+
buildAppStartCommand(input),
|
|
27133
27210
|
`HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
|
|
27134
27211
|
].filter(Boolean).join(`
|
|
27135
27212
|
`);
|
|
27136
27213
|
}
|
|
27214
|
+
function buildAppStartCommand(input) {
|
|
27215
|
+
if (!input.appStartCommand)
|
|
27216
|
+
return;
|
|
27217
|
+
const waitUrl = input.appWaitUrl ?? input.appUrl ?? input.runOptions.url;
|
|
27218
|
+
const waitTimeoutMs = input.appWaitTimeoutMs ?? 120000;
|
|
27219
|
+
return [
|
|
27220
|
+
`( ${input.appStartCommand} ) > ${shellQuote(`${input.stateRemoteDir}/app.log`)} 2>&1 &`,
|
|
27221
|
+
"APP_PID=$!",
|
|
27222
|
+
`echo "$APP_PID" > ${shellQuote(`${input.stateRemoteDir}/app.pid`)}`,
|
|
27223
|
+
`trap 'kill "$APP_PID" 2>/dev/null || true' EXIT`,
|
|
27224
|
+
waitUrl ? buildWaitForUrlCommand(waitUrl, waitTimeoutMs) : undefined
|
|
27225
|
+
].filter(Boolean).join(`
|
|
27226
|
+
`);
|
|
27227
|
+
}
|
|
27228
|
+
function buildWaitForUrlCommand(url, timeoutMs) {
|
|
27229
|
+
const script = `
|
|
27230
|
+
const url = ${JSON.stringify(url)};
|
|
27231
|
+
const timeoutMs = ${JSON.stringify(timeoutMs)};
|
|
27232
|
+
const deadline = Date.now() + timeoutMs;
|
|
27233
|
+
while (Date.now() <= deadline) {
|
|
27234
|
+
try {
|
|
27235
|
+
const response = await fetch(url);
|
|
27236
|
+
if (response.status >= 200 && response.status < 500) process.exit(0);
|
|
27237
|
+
} catch {}
|
|
27238
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
27239
|
+
}
|
|
27240
|
+
console.error(\`Timed out waiting for \${url} after \${timeoutMs}ms\`);
|
|
27241
|
+
process.exit(1);
|
|
27242
|
+
`.trim();
|
|
27243
|
+
return `bun -e ${shellQuote(script)}`;
|
|
27244
|
+
}
|
|
27137
27245
|
async function runViaSandbox(plan, dependencies) {
|
|
27138
27246
|
if (!plan.sandbox)
|
|
27139
27247
|
throw new Error("Workflow does not have a sandbox plan");
|
|
@@ -27191,11 +27299,22 @@ async function resolveSandboxesRuntime(dependencies) {
|
|
|
27191
27299
|
function shellQuote(value) {
|
|
27192
27300
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
27193
27301
|
}
|
|
27302
|
+
var APP_SOURCE_EXCLUDES;
|
|
27194
27303
|
var init_workflow_runner = __esm(() => {
|
|
27195
27304
|
init_database();
|
|
27196
27305
|
init_workflows();
|
|
27197
27306
|
init_personas();
|
|
27198
27307
|
init_runner();
|
|
27308
|
+
APP_SOURCE_EXCLUDES = [
|
|
27309
|
+
"node_modules",
|
|
27310
|
+
".git",
|
|
27311
|
+
"dist",
|
|
27312
|
+
".next",
|
|
27313
|
+
".turbo",
|
|
27314
|
+
".cache",
|
|
27315
|
+
".venv",
|
|
27316
|
+
"__pycache__"
|
|
27317
|
+
];
|
|
27199
27318
|
});
|
|
27200
27319
|
|
|
27201
27320
|
// src/lib/ci.ts
|
|
@@ -27760,9 +27879,9 @@ import { webcrypto as crypto2 } from "crypto";
|
|
|
27760
27879
|
import { existsSync as existsSync42, writeFileSync as writeFileSync32, readFileSync as readFileSync22, mkdirSync as mkdirSync32 } from "fs";
|
|
27761
27880
|
import { join as join42 } from "path";
|
|
27762
27881
|
import { Database as Database4 } from "bun:sqlite";
|
|
27763
|
-
import { existsSync as
|
|
27882
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync14 } from "fs";
|
|
27764
27883
|
import { dirname as dirname4, join as join19, resolve as resolve2 } from "path";
|
|
27765
|
-
import { existsSync as existsSync22, writeFileSync as
|
|
27884
|
+
import { existsSync as existsSync22, writeFileSync as writeFileSync6 } from "fs";
|
|
27766
27885
|
import { join as join22 } from "path";
|
|
27767
27886
|
import { execSync as execSync3, execFileSync } from "child_process";
|
|
27768
27887
|
import { existsSync as existsSync32, readFileSync as readFileSync7, writeFileSync as writeFileSync22, mkdirSync as mkdirSync22 } from "fs";
|
|
@@ -27774,7 +27893,7 @@ import * as zlib from "zlib";
|
|
|
27774
27893
|
import { Readable } from "stream";
|
|
27775
27894
|
import { Writable } from "stream";
|
|
27776
27895
|
import { createHash as createHash22 } from "crypto";
|
|
27777
|
-
import { mkdirSync as mkdirSync4, statSync as
|
|
27896
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync42 } from "fs";
|
|
27778
27897
|
import { dirname as dirname42, join as join62, relative as relative3 } from "path";
|
|
27779
27898
|
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
27780
27899
|
import { existsSync as existsSync62, readdirSync as readdirSync5, readFileSync as readFileSync42, statSync as statSync22 } from "fs";
|
|
@@ -27942,8 +28061,8 @@ function ensureDir2(filePath) {
|
|
|
27942
28061
|
if (filePath === ":memory:")
|
|
27943
28062
|
return;
|
|
27944
28063
|
const dir = dirname4(resolve2(filePath));
|
|
27945
|
-
if (!
|
|
27946
|
-
|
|
28064
|
+
if (!existsSync17(dir)) {
|
|
28065
|
+
mkdirSync14(dir, { recursive: true });
|
|
27947
28066
|
}
|
|
27948
28067
|
}
|
|
27949
28068
|
function getDatabase2(path) {
|
|
@@ -27981,7 +28100,7 @@ function gitInit(project) {
|
|
|
27981
28100
|
execSync3("git init", { cwd: path, stdio: "pipe" });
|
|
27982
28101
|
const gitignorePath = join22(path, ".gitignore");
|
|
27983
28102
|
if (!existsSync22(gitignorePath)) {
|
|
27984
|
-
|
|
28103
|
+
writeFileSync6(gitignorePath, GITIGNORE_TEMPLATE, "utf-8");
|
|
27985
28104
|
}
|
|
27986
28105
|
const projectJson = {
|
|
27987
28106
|
id,
|
|
@@ -27990,7 +28109,7 @@ function gitInit(project) {
|
|
|
27990
28109
|
created_at: project.created_at,
|
|
27991
28110
|
integrations: project.integrations ?? {}
|
|
27992
28111
|
};
|
|
27993
|
-
|
|
28112
|
+
writeFileSync6(join22(path, ".project.json"), JSON.stringify(projectJson, null, 2) + `
|
|
27994
28113
|
`, "utf-8");
|
|
27995
28114
|
execSync3("git add .gitignore .project.json", { cwd: path, stdio: "pipe" });
|
|
27996
28115
|
execSync3(`git commit -m "chore: init project ${name}"`, {
|
|
@@ -29302,7 +29421,7 @@ async function collectLocalFiles(rootPath) {
|
|
|
29302
29421
|
if (entry.isDirectory()) {
|
|
29303
29422
|
await walk(fullPath);
|
|
29304
29423
|
} else if (entry.isFile()) {
|
|
29305
|
-
const stat =
|
|
29424
|
+
const stat = statSync3(fullPath);
|
|
29306
29425
|
if (stat.size > MAX_FILE_SIZE)
|
|
29307
29426
|
continue;
|
|
29308
29427
|
const relPath = relative3(rootPath, fullPath);
|
|
@@ -94346,7 +94465,7 @@ import chalk6 from "chalk";
|
|
|
94346
94465
|
// package.json
|
|
94347
94466
|
var package_default = {
|
|
94348
94467
|
name: "@hasna/testers",
|
|
94349
|
-
version: "0.0.
|
|
94468
|
+
version: "0.0.46",
|
|
94350
94469
|
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
94351
94470
|
type: "module",
|
|
94352
94471
|
main: "dist/index.js",
|
|
@@ -94445,7 +94564,7 @@ init_todos_connector();
|
|
|
94445
94564
|
init_browser();
|
|
94446
94565
|
import { render, Box, Text, useInput, useApp } from "ink";
|
|
94447
94566
|
import React, { useState } from "react";
|
|
94448
|
-
import { readFileSync as readFileSync10, readdirSync as readdirSync6, writeFileSync as
|
|
94567
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
94449
94568
|
import { createInterface } from "readline";
|
|
94450
94569
|
import { join as join20, resolve as resolve4 } from "path";
|
|
94451
94570
|
|
|
@@ -96533,18 +96652,18 @@ init_ci();
|
|
|
96533
96652
|
init_assertions();
|
|
96534
96653
|
init_paths();
|
|
96535
96654
|
init_sessions();
|
|
96536
|
-
import { existsSync as
|
|
96655
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync15 } from "fs";
|
|
96537
96656
|
|
|
96538
96657
|
// src/lib/repo-discovery.ts
|
|
96539
96658
|
init_paths();
|
|
96540
|
-
import { existsSync as
|
|
96659
|
+
import { existsSync as existsSync15, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync2, writeFileSync as writeFileSync4, mkdirSync as mkdirSync12, unlinkSync } from "fs";
|
|
96541
96660
|
import { createHash } from "crypto";
|
|
96542
96661
|
import { join as join17, resolve, relative as relative2 } from "path";
|
|
96543
96662
|
function getCacheDir() {
|
|
96544
96663
|
const testersDir = getTestersDir();
|
|
96545
96664
|
const cacheDir = join17(testersDir, "repo-index");
|
|
96546
|
-
if (!
|
|
96547
|
-
|
|
96665
|
+
if (!existsSync15(cacheDir)) {
|
|
96666
|
+
mkdirSync12(cacheDir, { recursive: true });
|
|
96548
96667
|
}
|
|
96549
96668
|
return cacheDir;
|
|
96550
96669
|
}
|
|
@@ -96557,10 +96676,10 @@ function getCachePath(repoPath) {
|
|
|
96557
96676
|
function isCacheStale(cached, repoPath) {
|
|
96558
96677
|
for (const spec of cached.specs) {
|
|
96559
96678
|
const fullPath = join17(repoPath, spec.file);
|
|
96560
|
-
if (!
|
|
96679
|
+
if (!existsSync15(fullPath))
|
|
96561
96680
|
return true;
|
|
96562
96681
|
try {
|
|
96563
|
-
const stat =
|
|
96682
|
+
const stat = statSync2(fullPath);
|
|
96564
96683
|
if (stat.mtimeMs !== spec.mtimeMs)
|
|
96565
96684
|
return true;
|
|
96566
96685
|
} catch {
|
|
@@ -96569,10 +96688,10 @@ function isCacheStale(cached, repoPath) {
|
|
|
96569
96688
|
}
|
|
96570
96689
|
if (cached.configPath) {
|
|
96571
96690
|
const configFullPath = join17(repoPath, cached.configPath);
|
|
96572
|
-
if (!
|
|
96691
|
+
if (!existsSync15(configFullPath))
|
|
96573
96692
|
return true;
|
|
96574
96693
|
try {
|
|
96575
|
-
|
|
96694
|
+
statSync2(configFullPath);
|
|
96576
96695
|
const age = Date.now() - new Date(cached.snapshotAt).getTime();
|
|
96577
96696
|
if (age > 3600000)
|
|
96578
96697
|
return true;
|
|
@@ -96584,7 +96703,7 @@ function isCacheStale(cached, repoPath) {
|
|
|
96584
96703
|
}
|
|
96585
96704
|
function loadCache(repoPath) {
|
|
96586
96705
|
const cachePath = getCachePath(repoPath);
|
|
96587
|
-
if (!
|
|
96706
|
+
if (!existsSync15(cachePath))
|
|
96588
96707
|
return null;
|
|
96589
96708
|
try {
|
|
96590
96709
|
const raw = JSON.parse(readFileSync6(cachePath, "utf-8"));
|
|
@@ -96595,14 +96714,14 @@ function loadCache(repoPath) {
|
|
|
96595
96714
|
}
|
|
96596
96715
|
function saveCache(snapshot) {
|
|
96597
96716
|
const cachePath = getCachePath(snapshot.repoPath);
|
|
96598
|
-
|
|
96717
|
+
writeFileSync4(cachePath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
96599
96718
|
}
|
|
96600
96719
|
function detectPackageManager(repoPath) {
|
|
96601
96720
|
const result = {
|
|
96602
|
-
npm:
|
|
96603
|
-
yarn:
|
|
96604
|
-
pnpm:
|
|
96605
|
-
bun:
|
|
96721
|
+
npm: existsSync15(join17(repoPath, "package-lock.json")),
|
|
96722
|
+
yarn: existsSync15(join17(repoPath, "yarn.lock")),
|
|
96723
|
+
pnpm: existsSync15(join17(repoPath, "pnpm-lock.yaml")),
|
|
96724
|
+
bun: existsSync15(join17(repoPath, "bun.lockb")) || existsSync15(join17(repoPath, "bun.lock")),
|
|
96606
96725
|
preferred: "npm"
|
|
96607
96726
|
};
|
|
96608
96727
|
if (result.bun)
|
|
@@ -96617,7 +96736,7 @@ function detectPackageManager(repoPath) {
|
|
|
96617
96736
|
}
|
|
96618
96737
|
function detectDevScripts(repoPath) {
|
|
96619
96738
|
const pkgPath = join17(repoPath, "package.json");
|
|
96620
|
-
if (!
|
|
96739
|
+
if (!existsSync15(pkgPath)) {
|
|
96621
96740
|
return { dev: null, test: null, seed: null, build: null };
|
|
96622
96741
|
}
|
|
96623
96742
|
let scripts;
|
|
@@ -96643,7 +96762,7 @@ function findPlaywrightConfig(repoPath) {
|
|
|
96643
96762
|
"playwright-ct.config.js"
|
|
96644
96763
|
];
|
|
96645
96764
|
for (const name of candidates) {
|
|
96646
|
-
if (
|
|
96765
|
+
if (existsSync15(join17(repoPath, name)))
|
|
96647
96766
|
return name;
|
|
96648
96767
|
}
|
|
96649
96768
|
return null;
|
|
@@ -96702,7 +96821,7 @@ function findSpecFiles(repoPath, globPatterns) {
|
|
|
96702
96821
|
const dirsToSearch = ["", ".", "tests", "e2e", "test", "__tests__", "specs", "src"];
|
|
96703
96822
|
for (const dir of dirsToSearch) {
|
|
96704
96823
|
const searchDir = dir ? join17(repoPath, dir) : repoPath;
|
|
96705
|
-
if (!
|
|
96824
|
+
if (!existsSync15(searchDir))
|
|
96706
96825
|
continue;
|
|
96707
96826
|
try {
|
|
96708
96827
|
const files = walkDir(searchDir);
|
|
@@ -96714,7 +96833,7 @@ function findSpecFiles(repoPath, globPatterns) {
|
|
|
96714
96833
|
seen.add(relativePath);
|
|
96715
96834
|
const content = readFileSync6(file, "utf-8");
|
|
96716
96835
|
const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
96717
|
-
const stat =
|
|
96836
|
+
const stat = statSync2(file);
|
|
96718
96837
|
specs.push({
|
|
96719
96838
|
file: relativePath,
|
|
96720
96839
|
fromGlob: pattern,
|
|
@@ -96754,7 +96873,7 @@ function matchesGlob(filePath, pattern) {
|
|
|
96754
96873
|
}
|
|
96755
96874
|
function detectSuggestedUrl(repoPath) {
|
|
96756
96875
|
const pkgPath = join17(repoPath, "package.json");
|
|
96757
|
-
if (!
|
|
96876
|
+
if (!existsSync15(pkgPath))
|
|
96758
96877
|
return null;
|
|
96759
96878
|
try {
|
|
96760
96879
|
const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
@@ -96774,10 +96893,10 @@ function detectSuggestedUrl(repoPath) {
|
|
|
96774
96893
|
}
|
|
96775
96894
|
function checkPlaywrightBrowserInstalled(repoPath) {
|
|
96776
96895
|
const cacheDir = join17(repoPath, "node_modules", ".cache", "ms-playwright");
|
|
96777
|
-
if (
|
|
96896
|
+
if (existsSync15(cacheDir))
|
|
96778
96897
|
return true;
|
|
96779
96898
|
const globalCache = join17(repoPath, ".cache", "ms-playwright");
|
|
96780
|
-
if (
|
|
96899
|
+
if (existsSync15(globalCache))
|
|
96781
96900
|
return true;
|
|
96782
96901
|
return false;
|
|
96783
96902
|
}
|
|
@@ -96817,7 +96936,7 @@ function discoverRepo(opts) {
|
|
|
96817
96936
|
const specs = findSpecFiles(repoPath, globPatterns);
|
|
96818
96937
|
const packageManager = detectPackageManager(repoPath);
|
|
96819
96938
|
const devScripts = detectDevScripts(repoPath);
|
|
96820
|
-
const playwrightInstalled =
|
|
96939
|
+
const playwrightInstalled = existsSync15(join17(repoPath, "node_modules", "playwright")) || existsSync15(join17(repoPath, "node_modules", "@playwright", "test"));
|
|
96821
96940
|
const browsersInstalled = checkPlaywrightBrowserInstalled(repoPath);
|
|
96822
96941
|
const configExists = configPath !== null;
|
|
96823
96942
|
const specsFound = specs.length > 0;
|
|
@@ -96876,11 +96995,11 @@ function discoverRepo(opts) {
|
|
|
96876
96995
|
}
|
|
96877
96996
|
function clearDiscoveryCache(repoPath) {
|
|
96878
96997
|
const cacheDir = getCacheDir();
|
|
96879
|
-
if (!
|
|
96998
|
+
if (!existsSync15(cacheDir))
|
|
96880
96999
|
return;
|
|
96881
97000
|
if (repoPath) {
|
|
96882
97001
|
const cachePath = getCachePath(repoPath);
|
|
96883
|
-
if (
|
|
97002
|
+
if (existsSync15(cachePath)) {
|
|
96884
97003
|
unlinkSync(cachePath);
|
|
96885
97004
|
}
|
|
96886
97005
|
} else {
|
|
@@ -96893,7 +97012,7 @@ function clearDiscoveryCache(repoPath) {
|
|
|
96893
97012
|
}
|
|
96894
97013
|
function getDiscoveryCacheInfo(repoPath) {
|
|
96895
97014
|
const cachePath = getCachePath(repoPath);
|
|
96896
|
-
if (!
|
|
97015
|
+
if (!existsSync15(cachePath))
|
|
96897
97016
|
return null;
|
|
96898
97017
|
const cached = loadCache(repoPath);
|
|
96899
97018
|
if (!cached)
|
|
@@ -96910,11 +97029,11 @@ init_runs();
|
|
|
96910
97029
|
init_database();
|
|
96911
97030
|
init_paths();
|
|
96912
97031
|
import { execSync as execSync2 } from "child_process";
|
|
96913
|
-
import { existsSync as
|
|
97032
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync13, writeFileSync as writeFileSync5 } from "fs";
|
|
96914
97033
|
import { join as join18 } from "path";
|
|
96915
97034
|
function resolvePlaywrightCmd(repoPath) {
|
|
96916
97035
|
const localPw = join18(repoPath, "node_modules", ".bin", "playwright");
|
|
96917
|
-
if (
|
|
97036
|
+
if (existsSync16(localPw)) {
|
|
96918
97037
|
return [localPw, "test"];
|
|
96919
97038
|
}
|
|
96920
97039
|
return ["npx", "playwright", "test"];
|
|
@@ -97110,9 +97229,9 @@ async function runRepoTests(opts) {
|
|
|
97110
97229
|
const resultRecord = { id: resultId };
|
|
97111
97230
|
if (result.stdout || result.stderr) {
|
|
97112
97231
|
const reportersDir = join18(getTestersDir(), "repo-run-output");
|
|
97113
|
-
|
|
97232
|
+
mkdirSync13(reportersDir, { recursive: true });
|
|
97114
97233
|
const outputFile = join18(reportersDir, `${resultRecord.id}.log`);
|
|
97115
|
-
|
|
97234
|
+
writeFileSync5(outputFile, `=== stdout ===
|
|
97116
97235
|
${result.stdout}
|
|
97117
97236
|
|
|
97118
97237
|
=== stderr ===
|
|
@@ -97223,6 +97342,29 @@ function splitCsvOption(value) {
|
|
|
97223
97342
|
const items = value?.split(",").map((item) => item.trim()).filter(Boolean) ?? [];
|
|
97224
97343
|
return items.length > 0 ? items : undefined;
|
|
97225
97344
|
}
|
|
97345
|
+
function describeStoredAssertion(value) {
|
|
97346
|
+
if (typeof value === "string")
|
|
97347
|
+
return value;
|
|
97348
|
+
try {
|
|
97349
|
+
return JSON.stringify(value);
|
|
97350
|
+
} catch {
|
|
97351
|
+
return String(value);
|
|
97352
|
+
}
|
|
97353
|
+
}
|
|
97354
|
+
function validateStoredAssertion(value) {
|
|
97355
|
+
if (typeof value === "string") {
|
|
97356
|
+
try {
|
|
97357
|
+
parseAssertionString(value);
|
|
97358
|
+
return null;
|
|
97359
|
+
} catch {
|
|
97360
|
+
return value;
|
|
97361
|
+
}
|
|
97362
|
+
}
|
|
97363
|
+
if (value && typeof value === "object" && typeof value.type === "string") {
|
|
97364
|
+
return null;
|
|
97365
|
+
}
|
|
97366
|
+
return describeStoredAssertion(value);
|
|
97367
|
+
}
|
|
97226
97368
|
function AddForm({ onComplete }) {
|
|
97227
97369
|
const { exit } = useApp();
|
|
97228
97370
|
const [state, setState] = useState({
|
|
@@ -97496,7 +97638,7 @@ program2.command("prod-debug <target>").description("Create a safe production de
|
|
|
97496
97638
|
}, config2.prodDebug);
|
|
97497
97639
|
const output = opts.json ? JSON.stringify(plan, null, 2) : formatProdDebugPlan(plan);
|
|
97498
97640
|
if (opts.output) {
|
|
97499
|
-
|
|
97641
|
+
writeFileSync7(resolve4(opts.output), output + `
|
|
97500
97642
|
`);
|
|
97501
97643
|
} else {
|
|
97502
97644
|
log(output);
|
|
@@ -97506,7 +97648,7 @@ var CONFIG_DIR5 = getTestersDir();
|
|
|
97506
97648
|
var CONFIG_PATH4 = join20(CONFIG_DIR5, "config.json");
|
|
97507
97649
|
function getActiveProject() {
|
|
97508
97650
|
try {
|
|
97509
|
-
if (
|
|
97651
|
+
if (existsSync18(CONFIG_PATH4)) {
|
|
97510
97652
|
const raw = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
|
|
97511
97653
|
return raw.activeProject ?? undefined;
|
|
97512
97654
|
}
|
|
@@ -97887,11 +98029,9 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
97887
98029
|
for (const s2 of dryScenarios) {
|
|
97888
98030
|
const assertionErrors = [];
|
|
97889
98031
|
for (const a2 of s2.assertions ?? []) {
|
|
97890
|
-
|
|
97891
|
-
|
|
97892
|
-
|
|
97893
|
-
assertionErrors.push(a2);
|
|
97894
|
-
}
|
|
98032
|
+
const error40 = validateStoredAssertion(a2);
|
|
98033
|
+
if (error40)
|
|
98034
|
+
assertionErrors.push(error40);
|
|
97895
98035
|
}
|
|
97896
98036
|
let authOk = true;
|
|
97897
98037
|
if (s2.authPreset) {
|
|
@@ -98062,7 +98202,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
98062
98202
|
if (opts.json || opts.output) {
|
|
98063
98203
|
const jsonOutput = formatJSON(run3, results2);
|
|
98064
98204
|
if (opts.output) {
|
|
98065
|
-
|
|
98205
|
+
writeFileSync7(opts.output, jsonOutput, "utf-8");
|
|
98066
98206
|
log(chalk6.green(`Results written to ${opts.output}`));
|
|
98067
98207
|
}
|
|
98068
98208
|
if (opts.json) {
|
|
@@ -98171,7 +98311,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
98171
98311
|
if (opts.json || opts.output) {
|
|
98172
98312
|
const jsonOutput = formatJSON(run2, results);
|
|
98173
98313
|
if (opts.output) {
|
|
98174
|
-
|
|
98314
|
+
writeFileSync7(opts.output, jsonOutput, "utf-8");
|
|
98175
98315
|
log(chalk6.green(`Results written to ${opts.output}`));
|
|
98176
98316
|
}
|
|
98177
98317
|
if (opts.json) {
|
|
@@ -98424,13 +98564,13 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
|
|
|
98424
98564
|
if (fmt === "json") {
|
|
98425
98565
|
const outputPath = opts.output ?? "testers-export.json";
|
|
98426
98566
|
const data = JSON.stringify(scenarios, null, 2);
|
|
98427
|
-
|
|
98567
|
+
writeFileSync7(outputPath, data, "utf-8");
|
|
98428
98568
|
log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${resolve4(outputPath)}`));
|
|
98429
98569
|
return;
|
|
98430
98570
|
}
|
|
98431
98571
|
const outputDir = opts.output ?? ".";
|
|
98432
|
-
if (!
|
|
98433
|
-
|
|
98572
|
+
if (!existsSync18(outputDir)) {
|
|
98573
|
+
mkdirSync15(outputDir, { recursive: true });
|
|
98434
98574
|
}
|
|
98435
98575
|
for (const s2 of scenarios) {
|
|
98436
98576
|
const lines = [];
|
|
@@ -98458,7 +98598,7 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
|
|
|
98458
98598
|
}
|
|
98459
98599
|
const safeFilename = s2.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
|
|
98460
98600
|
const filePath = join20(outputDir, `${s2.shortId}-${safeFilename}.md`);
|
|
98461
|
-
|
|
98601
|
+
writeFileSync7(filePath, lines.join(`
|
|
98462
98602
|
`), "utf-8");
|
|
98463
98603
|
log(chalk6.dim(` ${s2.shortId}: ${s2.name} \u2192 ${filePath}`));
|
|
98464
98604
|
}
|
|
@@ -98632,17 +98772,17 @@ projectCmd.command("export-open <id>").description("Register a testers project i
|
|
|
98632
98772
|
projectCmd.command("use <name>").description("Set active project (find or create)").option("--json", "Output as JSON", false).action((name21, opts) => {
|
|
98633
98773
|
try {
|
|
98634
98774
|
const project = ensureProject(name21, process.cwd());
|
|
98635
|
-
if (!
|
|
98636
|
-
|
|
98775
|
+
if (!existsSync18(CONFIG_DIR5)) {
|
|
98776
|
+
mkdirSync15(CONFIG_DIR5, { recursive: true });
|
|
98637
98777
|
}
|
|
98638
98778
|
let config2 = {};
|
|
98639
|
-
if (
|
|
98779
|
+
if (existsSync18(CONFIG_PATH4)) {
|
|
98640
98780
|
try {
|
|
98641
98781
|
config2 = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
|
|
98642
98782
|
} catch {}
|
|
98643
98783
|
}
|
|
98644
98784
|
config2.activeProject = project.id;
|
|
98645
|
-
|
|
98785
|
+
writeFileSync7(CONFIG_PATH4, JSON.stringify(config2, null, 2), "utf-8");
|
|
98646
98786
|
if (opts.json) {
|
|
98647
98787
|
log(JSON.stringify({ activeProject: project.id, project }, null, 2));
|
|
98648
98788
|
return;
|
|
@@ -99251,10 +99391,10 @@ program2.command("ci [provider]").description("Print or write a CI workflow (def
|
|
|
99251
99391
|
if (opts.output) {
|
|
99252
99392
|
const outPath = resolve4(opts.output);
|
|
99253
99393
|
const outDir = outPath.replace(/\/[^/]*$/, "");
|
|
99254
|
-
if (outDir && !
|
|
99255
|
-
|
|
99394
|
+
if (outDir && !existsSync18(outDir)) {
|
|
99395
|
+
mkdirSync15(outDir, { recursive: true });
|
|
99256
99396
|
}
|
|
99257
|
-
|
|
99397
|
+
writeFileSync7(outPath, workflow, "utf-8");
|
|
99258
99398
|
log(chalk6.green(`Workflow written to ${outPath}`));
|
|
99259
99399
|
return;
|
|
99260
99400
|
}
|
|
@@ -99286,11 +99426,11 @@ program2.command("init").description("Initialize a new testing project").option(
|
|
|
99286
99426
|
}
|
|
99287
99427
|
if (opts.ci === "github") {
|
|
99288
99428
|
const workflowDir = join20(process.cwd(), ".github", "workflows");
|
|
99289
|
-
if (!
|
|
99290
|
-
|
|
99429
|
+
if (!existsSync18(workflowDir)) {
|
|
99430
|
+
mkdirSync15(workflowDir, { recursive: true });
|
|
99291
99431
|
}
|
|
99292
99432
|
const workflowPath = join20(workflowDir, "testers.yml");
|
|
99293
|
-
|
|
99433
|
+
writeFileSync7(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
|
|
99294
99434
|
log(` CI: ${chalk6.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
|
|
99295
99435
|
} else if (opts.ci) {
|
|
99296
99436
|
log(chalk6.yellow(` Unknown CI provider: ${opts.ci}. Supported: github`));
|
|
@@ -99483,7 +99623,7 @@ program2.command("quick-qa <url>").alias("quick-check").description("Run a fast
|
|
|
99483
99623
|
wcagLevel
|
|
99484
99624
|
});
|
|
99485
99625
|
if (opts.output) {
|
|
99486
|
-
|
|
99626
|
+
writeFileSync7(resolve4(opts.output), JSON.stringify(result, null, 2));
|
|
99487
99627
|
}
|
|
99488
99628
|
if (opts.json) {
|
|
99489
99629
|
log(JSON.stringify(result, null, 2));
|
|
@@ -99534,7 +99674,7 @@ program2.command("report [run-id]").description("Generate HTML test report or co
|
|
|
99534
99674
|
format
|
|
99535
99675
|
});
|
|
99536
99676
|
if (opts.output && opts.output !== "report.html") {
|
|
99537
|
-
|
|
99677
|
+
writeFileSync7(opts.output, content, "utf-8");
|
|
99538
99678
|
const absPath2 = resolve4(opts.output);
|
|
99539
99679
|
log(chalk6.green(`Compliance report written to ${absPath2}`));
|
|
99540
99680
|
} else {
|
|
@@ -99548,7 +99688,7 @@ program2.command("report [run-id]").description("Generate HTML test report or co
|
|
|
99548
99688
|
} else {
|
|
99549
99689
|
html = generateHtmlReport(runId);
|
|
99550
99690
|
}
|
|
99551
|
-
|
|
99691
|
+
writeFileSync7(opts.output, html, "utf-8");
|
|
99552
99692
|
const absPath = resolve4(opts.output);
|
|
99553
99693
|
log(chalk6.green(`Report generated: ${absPath}`));
|
|
99554
99694
|
if (opts.open) {
|
|
@@ -100828,7 +100968,7 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
|
|
|
100828
100968
|
}, []).option("--priority <level>", "Scenario priority").option("--persona <ids>", "Comma-separated persona IDs").option("--goal <prompt>", "Goal prompt for the agentic testing loop").option("--success <criteria>", "Success criteria (repeatable)", (val, acc) => {
|
|
100829
100969
|
acc.push(val);
|
|
100830
100970
|
return acc;
|
|
100831
|
-
}, []).option("--max-iterations <n>", "Goal-loop iteration cap", "10").option("--target <target>", "Execution target: local or sandbox", "local").option("--sandbox-provider <provider>", "Sandbox provider: e2b, daytona, or modal").option("--sandbox-image <image>", "Sandbox image/template").option("--sandbox-remote-dir <path>", "Remote working directory for sandbox runs").option("--sandbox-cleanup <mode>", "Sandbox cleanup mode: delete, stop, or keep", "delete").option("--sandbox-sync <strategy>", "Sandbox upload sync strategy: rsync or archive", "rsync").option("--sandbox-setup-command <command>", "Shell command to run before testers in the sandbox").option("--sandbox-package <spec>", "Package spec to execute in the sandbox", "@hasna/testers").option("--e2b-template <name>", "Legacy alias for --sandbox-image").option("--timeout <ms>", "Workflow timeout").option("--json", "Output as JSON", false).action((name21, opts) => {
|
|
100971
|
+
}, []).option("--max-iterations <n>", "Goal-loop iteration cap", "10").option("--target <target>", "Execution target: local or sandbox", "local").option("--sandbox-provider <provider>", "Sandbox provider: e2b, daytona, or modal").option("--sandbox-image <image>", "Sandbox image/template").option("--sandbox-remote-dir <path>", "Remote working directory for sandbox runs").option("--sandbox-cleanup <mode>", "Sandbox cleanup mode: delete, stop, or keep", "delete").option("--sandbox-sync <strategy>", "Sandbox upload sync strategy: rsync or archive", "rsync").option("--sandbox-setup-command <command>", "Shell command to run before testers in the sandbox").option("--sandbox-package <spec>", "Package spec to execute in the sandbox", "@hasna/testers").option("--sandbox-app-source <path>", "Local app source directory to upload into the sandbox").option("--sandbox-app-remote-dir <path>", "Remote app directory inside the sandbox (default: <sandbox-remote-dir>/app)").option("--sandbox-app-start-command <command>", "Shell command to start the app before testers runs").option("--sandbox-app-url <url>", "URL testers should target inside the sandbox after the app starts").option("--sandbox-app-wait-url <url>", "URL to poll before starting testers (defaults to --sandbox-app-url)").option("--sandbox-app-wait-timeout <ms>", "App readiness wait timeout in milliseconds").option("--e2b-template <name>", "Legacy alias for --sandbox-image").option("--timeout <ms>", "Workflow timeout").option("--json", "Output as JSON", false).action((name21, opts) => {
|
|
100832
100972
|
try {
|
|
100833
100973
|
const workflow = createTestingWorkflow({
|
|
100834
100974
|
name: name21,
|
|
@@ -100854,6 +100994,12 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
|
|
|
100854
100994
|
sandboxSyncStrategy: opts.sandboxSync,
|
|
100855
100995
|
setupCommand: opts.sandboxSetupCommand,
|
|
100856
100996
|
packageSpec: opts.sandboxPackage,
|
|
100997
|
+
appSourceDir: opts.sandboxAppSource,
|
|
100998
|
+
appRemoteDir: opts.sandboxAppRemoteDir,
|
|
100999
|
+
appStartCommand: opts.sandboxAppStartCommand,
|
|
101000
|
+
appUrl: opts.sandboxAppUrl,
|
|
101001
|
+
appWaitUrl: opts.sandboxAppWaitUrl,
|
|
101002
|
+
appWaitTimeoutMs: opts.sandboxAppWaitTimeout ? parseInt(opts.sandboxAppWaitTimeout, 10) : undefined,
|
|
100857
101003
|
timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined
|
|
100858
101004
|
}
|
|
100859
101005
|
});
|