@hasna/testers 0.0.45 → 0.0.47
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 +260 -74
- package/dist/index.js +174 -43
- package/dist/lib/workflow-runner.d.ts +6 -0
- package/dist/lib/workflow-runner.d.ts.map +1 -1
- package/dist/mcp/index.js +150 -17
- package/dist/server/index.js +144 -13
- 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");
|
|
@@ -27154,7 +27262,7 @@ async function runViaSandbox(plan, dependencies) {
|
|
|
27154
27262
|
workflowId: plan.workflow.id,
|
|
27155
27263
|
workflowName: plan.workflow.name
|
|
27156
27264
|
},
|
|
27157
|
-
sandboxEnvVars: plan.sandbox.env,
|
|
27265
|
+
sandboxEnvVars: resolveSandboxEnv(plan.sandbox.env),
|
|
27158
27266
|
cleanup: plan.sandbox.cleanup,
|
|
27159
27267
|
upload: {
|
|
27160
27268
|
localDir: bundle.localDir,
|
|
@@ -27180,6 +27288,19 @@ async function runViaSandbox(plan, dependencies) {
|
|
|
27180
27288
|
bundle.cleanup?.();
|
|
27181
27289
|
}
|
|
27182
27290
|
}
|
|
27291
|
+
function resolveSandboxEnv(env) {
|
|
27292
|
+
if (!env || Object.keys(env).length === 0)
|
|
27293
|
+
return;
|
|
27294
|
+
const resolved = {};
|
|
27295
|
+
for (const [key, value] of Object.entries(env)) {
|
|
27296
|
+
const resolvedValue = resolveCredential(value);
|
|
27297
|
+
if (resolvedValue === null) {
|
|
27298
|
+
throw new Error(`Missing sandbox env value for ${key}`);
|
|
27299
|
+
}
|
|
27300
|
+
resolved[key] = resolvedValue;
|
|
27301
|
+
}
|
|
27302
|
+
return resolved;
|
|
27303
|
+
}
|
|
27183
27304
|
async function resolveSandboxesRuntime(dependencies) {
|
|
27184
27305
|
if (dependencies.sandboxes)
|
|
27185
27306
|
return dependencies.sandboxes;
|
|
@@ -27191,11 +27312,23 @@ async function resolveSandboxesRuntime(dependencies) {
|
|
|
27191
27312
|
function shellQuote(value) {
|
|
27192
27313
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
27193
27314
|
}
|
|
27315
|
+
var APP_SOURCE_EXCLUDES;
|
|
27194
27316
|
var init_workflow_runner = __esm(() => {
|
|
27195
27317
|
init_database();
|
|
27196
27318
|
init_workflows();
|
|
27197
27319
|
init_personas();
|
|
27198
27320
|
init_runner();
|
|
27321
|
+
init_secrets_resolver();
|
|
27322
|
+
APP_SOURCE_EXCLUDES = [
|
|
27323
|
+
"node_modules",
|
|
27324
|
+
".git",
|
|
27325
|
+
"dist",
|
|
27326
|
+
".next",
|
|
27327
|
+
".turbo",
|
|
27328
|
+
".cache",
|
|
27329
|
+
".venv",
|
|
27330
|
+
"__pycache__"
|
|
27331
|
+
];
|
|
27199
27332
|
});
|
|
27200
27333
|
|
|
27201
27334
|
// src/lib/ci.ts
|
|
@@ -27760,9 +27893,9 @@ import { webcrypto as crypto2 } from "crypto";
|
|
|
27760
27893
|
import { existsSync as existsSync42, writeFileSync as writeFileSync32, readFileSync as readFileSync22, mkdirSync as mkdirSync32 } from "fs";
|
|
27761
27894
|
import { join as join42 } from "path";
|
|
27762
27895
|
import { Database as Database4 } from "bun:sqlite";
|
|
27763
|
-
import { existsSync as
|
|
27896
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync14 } from "fs";
|
|
27764
27897
|
import { dirname as dirname4, join as join19, resolve as resolve2 } from "path";
|
|
27765
|
-
import { existsSync as existsSync22, writeFileSync as
|
|
27898
|
+
import { existsSync as existsSync22, writeFileSync as writeFileSync6 } from "fs";
|
|
27766
27899
|
import { join as join22 } from "path";
|
|
27767
27900
|
import { execSync as execSync3, execFileSync } from "child_process";
|
|
27768
27901
|
import { existsSync as existsSync32, readFileSync as readFileSync7, writeFileSync as writeFileSync22, mkdirSync as mkdirSync22 } from "fs";
|
|
@@ -27774,7 +27907,7 @@ import * as zlib from "zlib";
|
|
|
27774
27907
|
import { Readable } from "stream";
|
|
27775
27908
|
import { Writable } from "stream";
|
|
27776
27909
|
import { createHash as createHash22 } from "crypto";
|
|
27777
|
-
import { mkdirSync as mkdirSync4, statSync as
|
|
27910
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync42 } from "fs";
|
|
27778
27911
|
import { dirname as dirname42, join as join62, relative as relative3 } from "path";
|
|
27779
27912
|
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
27780
27913
|
import { existsSync as existsSync62, readdirSync as readdirSync5, readFileSync as readFileSync42, statSync as statSync22 } from "fs";
|
|
@@ -27942,8 +28075,8 @@ function ensureDir2(filePath) {
|
|
|
27942
28075
|
if (filePath === ":memory:")
|
|
27943
28076
|
return;
|
|
27944
28077
|
const dir = dirname4(resolve2(filePath));
|
|
27945
|
-
if (!
|
|
27946
|
-
|
|
28078
|
+
if (!existsSync17(dir)) {
|
|
28079
|
+
mkdirSync14(dir, { recursive: true });
|
|
27947
28080
|
}
|
|
27948
28081
|
}
|
|
27949
28082
|
function getDatabase2(path) {
|
|
@@ -27981,7 +28114,7 @@ function gitInit(project) {
|
|
|
27981
28114
|
execSync3("git init", { cwd: path, stdio: "pipe" });
|
|
27982
28115
|
const gitignorePath = join22(path, ".gitignore");
|
|
27983
28116
|
if (!existsSync22(gitignorePath)) {
|
|
27984
|
-
|
|
28117
|
+
writeFileSync6(gitignorePath, GITIGNORE_TEMPLATE, "utf-8");
|
|
27985
28118
|
}
|
|
27986
28119
|
const projectJson = {
|
|
27987
28120
|
id,
|
|
@@ -27990,7 +28123,7 @@ function gitInit(project) {
|
|
|
27990
28123
|
created_at: project.created_at,
|
|
27991
28124
|
integrations: project.integrations ?? {}
|
|
27992
28125
|
};
|
|
27993
|
-
|
|
28126
|
+
writeFileSync6(join22(path, ".project.json"), JSON.stringify(projectJson, null, 2) + `
|
|
27994
28127
|
`, "utf-8");
|
|
27995
28128
|
execSync3("git add .gitignore .project.json", { cwd: path, stdio: "pipe" });
|
|
27996
28129
|
execSync3(`git commit -m "chore: init project ${name}"`, {
|
|
@@ -29302,7 +29435,7 @@ async function collectLocalFiles(rootPath) {
|
|
|
29302
29435
|
if (entry.isDirectory()) {
|
|
29303
29436
|
await walk(fullPath);
|
|
29304
29437
|
} else if (entry.isFile()) {
|
|
29305
|
-
const stat =
|
|
29438
|
+
const stat = statSync3(fullPath);
|
|
29306
29439
|
if (stat.size > MAX_FILE_SIZE)
|
|
29307
29440
|
continue;
|
|
29308
29441
|
const relPath = relative3(rootPath, fullPath);
|
|
@@ -94346,7 +94479,7 @@ import chalk6 from "chalk";
|
|
|
94346
94479
|
// package.json
|
|
94347
94480
|
var package_default = {
|
|
94348
94481
|
name: "@hasna/testers",
|
|
94349
|
-
version: "0.0.
|
|
94482
|
+
version: "0.0.47",
|
|
94350
94483
|
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
94351
94484
|
type: "module",
|
|
94352
94485
|
main: "dist/index.js",
|
|
@@ -94445,7 +94578,7 @@ init_todos_connector();
|
|
|
94445
94578
|
init_browser();
|
|
94446
94579
|
import { render, Box, Text, useInput, useApp } from "ink";
|
|
94447
94580
|
import React, { useState } from "react";
|
|
94448
|
-
import { readFileSync as readFileSync10, readdirSync as readdirSync6, writeFileSync as
|
|
94581
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
94449
94582
|
import { createInterface } from "readline";
|
|
94450
94583
|
import { join as join20, resolve as resolve4 } from "path";
|
|
94451
94584
|
|
|
@@ -96533,18 +96666,18 @@ init_ci();
|
|
|
96533
96666
|
init_assertions();
|
|
96534
96667
|
init_paths();
|
|
96535
96668
|
init_sessions();
|
|
96536
|
-
import { existsSync as
|
|
96669
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync15 } from "fs";
|
|
96537
96670
|
|
|
96538
96671
|
// src/lib/repo-discovery.ts
|
|
96539
96672
|
init_paths();
|
|
96540
|
-
import { existsSync as
|
|
96673
|
+
import { existsSync as existsSync15, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync2, writeFileSync as writeFileSync4, mkdirSync as mkdirSync12, unlinkSync } from "fs";
|
|
96541
96674
|
import { createHash } from "crypto";
|
|
96542
96675
|
import { join as join17, resolve, relative as relative2 } from "path";
|
|
96543
96676
|
function getCacheDir() {
|
|
96544
96677
|
const testersDir = getTestersDir();
|
|
96545
96678
|
const cacheDir = join17(testersDir, "repo-index");
|
|
96546
|
-
if (!
|
|
96547
|
-
|
|
96679
|
+
if (!existsSync15(cacheDir)) {
|
|
96680
|
+
mkdirSync12(cacheDir, { recursive: true });
|
|
96548
96681
|
}
|
|
96549
96682
|
return cacheDir;
|
|
96550
96683
|
}
|
|
@@ -96557,10 +96690,10 @@ function getCachePath(repoPath) {
|
|
|
96557
96690
|
function isCacheStale(cached, repoPath) {
|
|
96558
96691
|
for (const spec of cached.specs) {
|
|
96559
96692
|
const fullPath = join17(repoPath, spec.file);
|
|
96560
|
-
if (!
|
|
96693
|
+
if (!existsSync15(fullPath))
|
|
96561
96694
|
return true;
|
|
96562
96695
|
try {
|
|
96563
|
-
const stat =
|
|
96696
|
+
const stat = statSync2(fullPath);
|
|
96564
96697
|
if (stat.mtimeMs !== spec.mtimeMs)
|
|
96565
96698
|
return true;
|
|
96566
96699
|
} catch {
|
|
@@ -96569,10 +96702,10 @@ function isCacheStale(cached, repoPath) {
|
|
|
96569
96702
|
}
|
|
96570
96703
|
if (cached.configPath) {
|
|
96571
96704
|
const configFullPath = join17(repoPath, cached.configPath);
|
|
96572
|
-
if (!
|
|
96705
|
+
if (!existsSync15(configFullPath))
|
|
96573
96706
|
return true;
|
|
96574
96707
|
try {
|
|
96575
|
-
|
|
96708
|
+
statSync2(configFullPath);
|
|
96576
96709
|
const age = Date.now() - new Date(cached.snapshotAt).getTime();
|
|
96577
96710
|
if (age > 3600000)
|
|
96578
96711
|
return true;
|
|
@@ -96584,7 +96717,7 @@ function isCacheStale(cached, repoPath) {
|
|
|
96584
96717
|
}
|
|
96585
96718
|
function loadCache(repoPath) {
|
|
96586
96719
|
const cachePath = getCachePath(repoPath);
|
|
96587
|
-
if (!
|
|
96720
|
+
if (!existsSync15(cachePath))
|
|
96588
96721
|
return null;
|
|
96589
96722
|
try {
|
|
96590
96723
|
const raw = JSON.parse(readFileSync6(cachePath, "utf-8"));
|
|
@@ -96595,14 +96728,14 @@ function loadCache(repoPath) {
|
|
|
96595
96728
|
}
|
|
96596
96729
|
function saveCache(snapshot) {
|
|
96597
96730
|
const cachePath = getCachePath(snapshot.repoPath);
|
|
96598
|
-
|
|
96731
|
+
writeFileSync4(cachePath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
96599
96732
|
}
|
|
96600
96733
|
function detectPackageManager(repoPath) {
|
|
96601
96734
|
const result = {
|
|
96602
|
-
npm:
|
|
96603
|
-
yarn:
|
|
96604
|
-
pnpm:
|
|
96605
|
-
bun:
|
|
96735
|
+
npm: existsSync15(join17(repoPath, "package-lock.json")),
|
|
96736
|
+
yarn: existsSync15(join17(repoPath, "yarn.lock")),
|
|
96737
|
+
pnpm: existsSync15(join17(repoPath, "pnpm-lock.yaml")),
|
|
96738
|
+
bun: existsSync15(join17(repoPath, "bun.lockb")) || existsSync15(join17(repoPath, "bun.lock")),
|
|
96606
96739
|
preferred: "npm"
|
|
96607
96740
|
};
|
|
96608
96741
|
if (result.bun)
|
|
@@ -96617,7 +96750,7 @@ function detectPackageManager(repoPath) {
|
|
|
96617
96750
|
}
|
|
96618
96751
|
function detectDevScripts(repoPath) {
|
|
96619
96752
|
const pkgPath = join17(repoPath, "package.json");
|
|
96620
|
-
if (!
|
|
96753
|
+
if (!existsSync15(pkgPath)) {
|
|
96621
96754
|
return { dev: null, test: null, seed: null, build: null };
|
|
96622
96755
|
}
|
|
96623
96756
|
let scripts;
|
|
@@ -96643,7 +96776,7 @@ function findPlaywrightConfig(repoPath) {
|
|
|
96643
96776
|
"playwright-ct.config.js"
|
|
96644
96777
|
];
|
|
96645
96778
|
for (const name of candidates) {
|
|
96646
|
-
if (
|
|
96779
|
+
if (existsSync15(join17(repoPath, name)))
|
|
96647
96780
|
return name;
|
|
96648
96781
|
}
|
|
96649
96782
|
return null;
|
|
@@ -96702,7 +96835,7 @@ function findSpecFiles(repoPath, globPatterns) {
|
|
|
96702
96835
|
const dirsToSearch = ["", ".", "tests", "e2e", "test", "__tests__", "specs", "src"];
|
|
96703
96836
|
for (const dir of dirsToSearch) {
|
|
96704
96837
|
const searchDir = dir ? join17(repoPath, dir) : repoPath;
|
|
96705
|
-
if (!
|
|
96838
|
+
if (!existsSync15(searchDir))
|
|
96706
96839
|
continue;
|
|
96707
96840
|
try {
|
|
96708
96841
|
const files = walkDir(searchDir);
|
|
@@ -96714,7 +96847,7 @@ function findSpecFiles(repoPath, globPatterns) {
|
|
|
96714
96847
|
seen.add(relativePath);
|
|
96715
96848
|
const content = readFileSync6(file, "utf-8");
|
|
96716
96849
|
const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
96717
|
-
const stat =
|
|
96850
|
+
const stat = statSync2(file);
|
|
96718
96851
|
specs.push({
|
|
96719
96852
|
file: relativePath,
|
|
96720
96853
|
fromGlob: pattern,
|
|
@@ -96754,7 +96887,7 @@ function matchesGlob(filePath, pattern) {
|
|
|
96754
96887
|
}
|
|
96755
96888
|
function detectSuggestedUrl(repoPath) {
|
|
96756
96889
|
const pkgPath = join17(repoPath, "package.json");
|
|
96757
|
-
if (!
|
|
96890
|
+
if (!existsSync15(pkgPath))
|
|
96758
96891
|
return null;
|
|
96759
96892
|
try {
|
|
96760
96893
|
const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
@@ -96774,10 +96907,10 @@ function detectSuggestedUrl(repoPath) {
|
|
|
96774
96907
|
}
|
|
96775
96908
|
function checkPlaywrightBrowserInstalled(repoPath) {
|
|
96776
96909
|
const cacheDir = join17(repoPath, "node_modules", ".cache", "ms-playwright");
|
|
96777
|
-
if (
|
|
96910
|
+
if (existsSync15(cacheDir))
|
|
96778
96911
|
return true;
|
|
96779
96912
|
const globalCache = join17(repoPath, ".cache", "ms-playwright");
|
|
96780
|
-
if (
|
|
96913
|
+
if (existsSync15(globalCache))
|
|
96781
96914
|
return true;
|
|
96782
96915
|
return false;
|
|
96783
96916
|
}
|
|
@@ -96817,7 +96950,7 @@ function discoverRepo(opts) {
|
|
|
96817
96950
|
const specs = findSpecFiles(repoPath, globPatterns);
|
|
96818
96951
|
const packageManager = detectPackageManager(repoPath);
|
|
96819
96952
|
const devScripts = detectDevScripts(repoPath);
|
|
96820
|
-
const playwrightInstalled =
|
|
96953
|
+
const playwrightInstalled = existsSync15(join17(repoPath, "node_modules", "playwright")) || existsSync15(join17(repoPath, "node_modules", "@playwright", "test"));
|
|
96821
96954
|
const browsersInstalled = checkPlaywrightBrowserInstalled(repoPath);
|
|
96822
96955
|
const configExists = configPath !== null;
|
|
96823
96956
|
const specsFound = specs.length > 0;
|
|
@@ -96876,11 +97009,11 @@ function discoverRepo(opts) {
|
|
|
96876
97009
|
}
|
|
96877
97010
|
function clearDiscoveryCache(repoPath) {
|
|
96878
97011
|
const cacheDir = getCacheDir();
|
|
96879
|
-
if (!
|
|
97012
|
+
if (!existsSync15(cacheDir))
|
|
96880
97013
|
return;
|
|
96881
97014
|
if (repoPath) {
|
|
96882
97015
|
const cachePath = getCachePath(repoPath);
|
|
96883
|
-
if (
|
|
97016
|
+
if (existsSync15(cachePath)) {
|
|
96884
97017
|
unlinkSync(cachePath);
|
|
96885
97018
|
}
|
|
96886
97019
|
} else {
|
|
@@ -96893,7 +97026,7 @@ function clearDiscoveryCache(repoPath) {
|
|
|
96893
97026
|
}
|
|
96894
97027
|
function getDiscoveryCacheInfo(repoPath) {
|
|
96895
97028
|
const cachePath = getCachePath(repoPath);
|
|
96896
|
-
if (!
|
|
97029
|
+
if (!existsSync15(cachePath))
|
|
96897
97030
|
return null;
|
|
96898
97031
|
const cached = loadCache(repoPath);
|
|
96899
97032
|
if (!cached)
|
|
@@ -96910,11 +97043,11 @@ init_runs();
|
|
|
96910
97043
|
init_database();
|
|
96911
97044
|
init_paths();
|
|
96912
97045
|
import { execSync as execSync2 } from "child_process";
|
|
96913
|
-
import { existsSync as
|
|
97046
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync13, writeFileSync as writeFileSync5 } from "fs";
|
|
96914
97047
|
import { join as join18 } from "path";
|
|
96915
97048
|
function resolvePlaywrightCmd(repoPath) {
|
|
96916
97049
|
const localPw = join18(repoPath, "node_modules", ".bin", "playwright");
|
|
96917
|
-
if (
|
|
97050
|
+
if (existsSync16(localPw)) {
|
|
96918
97051
|
return [localPw, "test"];
|
|
96919
97052
|
}
|
|
96920
97053
|
return ["npx", "playwright", "test"];
|
|
@@ -97110,9 +97243,9 @@ async function runRepoTests(opts) {
|
|
|
97110
97243
|
const resultRecord = { id: resultId };
|
|
97111
97244
|
if (result.stdout || result.stderr) {
|
|
97112
97245
|
const reportersDir = join18(getTestersDir(), "repo-run-output");
|
|
97113
|
-
|
|
97246
|
+
mkdirSync13(reportersDir, { recursive: true });
|
|
97114
97247
|
const outputFile = join18(reportersDir, `${resultRecord.id}.log`);
|
|
97115
|
-
|
|
97248
|
+
writeFileSync5(outputFile, `=== stdout ===
|
|
97116
97249
|
${result.stdout}
|
|
97117
97250
|
|
|
97118
97251
|
=== stderr ===
|
|
@@ -97246,6 +97379,29 @@ function validateStoredAssertion(value) {
|
|
|
97246
97379
|
}
|
|
97247
97380
|
return describeStoredAssertion(value);
|
|
97248
97381
|
}
|
|
97382
|
+
function envCredentialRef(value) {
|
|
97383
|
+
const trimmed = value?.trim();
|
|
97384
|
+
if (!trimmed)
|
|
97385
|
+
return;
|
|
97386
|
+
return trimmed.startsWith("$") ? trimmed : `$${trimmed}`;
|
|
97387
|
+
}
|
|
97388
|
+
function parseSandboxEnv(values) {
|
|
97389
|
+
if (!values?.length)
|
|
97390
|
+
return;
|
|
97391
|
+
const env = {};
|
|
97392
|
+
for (const value of values) {
|
|
97393
|
+
const trimmed = value.trim();
|
|
97394
|
+
if (!trimmed)
|
|
97395
|
+
continue;
|
|
97396
|
+
const separator = trimmed.indexOf("=");
|
|
97397
|
+
const key = separator >= 0 ? trimmed.slice(0, separator).trim() : trimmed;
|
|
97398
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
97399
|
+
throw new Error(`Invalid sandbox env var name: ${key || value}`);
|
|
97400
|
+
}
|
|
97401
|
+
env[key] = separator >= 0 ? trimmed.slice(separator + 1) : `$${key}`;
|
|
97402
|
+
}
|
|
97403
|
+
return Object.keys(env).length > 0 ? env : undefined;
|
|
97404
|
+
}
|
|
97249
97405
|
function AddForm({ onComplete }) {
|
|
97250
97406
|
const { exit } = useApp();
|
|
97251
97407
|
const [state, setState] = useState({
|
|
@@ -97519,7 +97675,7 @@ program2.command("prod-debug <target>").description("Create a safe production de
|
|
|
97519
97675
|
}, config2.prodDebug);
|
|
97520
97676
|
const output = opts.json ? JSON.stringify(plan, null, 2) : formatProdDebugPlan(plan);
|
|
97521
97677
|
if (opts.output) {
|
|
97522
|
-
|
|
97678
|
+
writeFileSync7(resolve4(opts.output), output + `
|
|
97523
97679
|
`);
|
|
97524
97680
|
} else {
|
|
97525
97681
|
log(output);
|
|
@@ -97529,7 +97685,7 @@ var CONFIG_DIR5 = getTestersDir();
|
|
|
97529
97685
|
var CONFIG_PATH4 = join20(CONFIG_DIR5, "config.json");
|
|
97530
97686
|
function getActiveProject() {
|
|
97531
97687
|
try {
|
|
97532
|
-
if (
|
|
97688
|
+
if (existsSync18(CONFIG_PATH4)) {
|
|
97533
97689
|
const raw = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
|
|
97534
97690
|
return raw.activeProject ?? undefined;
|
|
97535
97691
|
}
|
|
@@ -97550,12 +97706,12 @@ program2.command("add [name]").alias("create").description("Create a new test sc
|
|
|
97550
97706
|
}, []).option("-t, --tag <tag>", "Tag (repeatable)", (val, acc) => {
|
|
97551
97707
|
acc.push(val);
|
|
97552
97708
|
return acc;
|
|
97553
|
-
}, []).option("-p, --priority <level>", "Priority level", "medium").option("-m, --model <model>", "AI model to use").option("--path <path>", "Target path on the URL").option("--auth", "Requires authentication", false).option("--timeout <ms>", "Timeout in milliseconds").option("--project <id>", "Project ID").option("--template <name>", "Seed scenarios from a template (auth, crud, forms, nav, a11y)").option("--assert <assertion>", "Structured assertion (repeatable). Formats: selector:<sel> visible, text:<sel> contains:<text>, no-console-errors, url:contains:<path>, title:contains:<text>, count:<sel> eq:<n>", (val, acc) => {
|
|
97709
|
+
}, []).option("-p, --priority <level>", "Priority level", "medium").option("-m, --model <model>", "AI model to use").option("--path <path>", "Target path on the URL").option("--auth", "Requires authentication", false).option("--auth-preset <name>", "Attach email/password/loginPath from a named auth preset").option("--timeout <ms>", "Timeout in milliseconds").option("--project <id>", "Project ID").option("--template <name>", "Seed scenarios from a template (auth, crud, forms, nav, a11y)").option("--assert <assertion>", "Structured assertion (repeatable). Formats: selector:<sel> visible, text:<sel> contains:<text>, no-console-errors, url:contains:<path>, title:contains:<text>, count:<sel> eq:<n>", (val, acc) => {
|
|
97554
97710
|
acc.push(val);
|
|
97555
97711
|
return acc;
|
|
97556
97712
|
}, []).action(async (name21, opts) => {
|
|
97557
97713
|
try {
|
|
97558
|
-
const hasFlags = opts.description || opts.steps?.length || opts.tag?.length || opts.model || opts.path || opts.auth || opts.timeout || opts.template || opts.assert?.length;
|
|
97714
|
+
const hasFlags = opts.description || opts.steps?.length || opts.tag?.length || opts.model || opts.path || opts.auth || opts.authPreset || opts.timeout || opts.template || opts.assert?.length;
|
|
97559
97715
|
if (!name21 && !hasFlags) {
|
|
97560
97716
|
const projectId2 = resolveProject2(opts.project);
|
|
97561
97717
|
await runInteractiveAdd(projectId2);
|
|
@@ -97580,6 +97736,11 @@ program2.command("add [name]").alias("create").description("Create a new test sc
|
|
|
97580
97736
|
}
|
|
97581
97737
|
const assertions = opts.assert.map(parseAssertionString);
|
|
97582
97738
|
const projectId = resolveProject2(opts.project);
|
|
97739
|
+
const authPreset = opts.authPreset ? getAuthPreset(opts.authPreset) : null;
|
|
97740
|
+
if (opts.authPreset && !authPreset) {
|
|
97741
|
+
logError(chalk6.red(`Auth preset not found: ${opts.authPreset}`));
|
|
97742
|
+
process.exit(1);
|
|
97743
|
+
}
|
|
97583
97744
|
const scenario = createScenario({
|
|
97584
97745
|
name: name21,
|
|
97585
97746
|
description: opts.description || name21,
|
|
@@ -97588,7 +97749,12 @@ program2.command("add [name]").alias("create").description("Create a new test sc
|
|
|
97588
97749
|
priority: opts.priority,
|
|
97589
97750
|
model: opts.model,
|
|
97590
97751
|
targetPath: opts.path,
|
|
97591
|
-
requiresAuth: opts.auth,
|
|
97752
|
+
requiresAuth: opts.auth || Boolean(authPreset),
|
|
97753
|
+
authConfig: authPreset ? {
|
|
97754
|
+
email: authPreset.email,
|
|
97755
|
+
password: authPreset.password,
|
|
97756
|
+
loginPath: authPreset.loginPath
|
|
97757
|
+
} : undefined,
|
|
97592
97758
|
timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
97593
97759
|
assertions: assertions.length > 0 ? assertions : undefined,
|
|
97594
97760
|
projectId
|
|
@@ -98083,7 +98249,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
98083
98249
|
if (opts.json || opts.output) {
|
|
98084
98250
|
const jsonOutput = formatJSON(run3, results2);
|
|
98085
98251
|
if (opts.output) {
|
|
98086
|
-
|
|
98252
|
+
writeFileSync7(opts.output, jsonOutput, "utf-8");
|
|
98087
98253
|
log(chalk6.green(`Results written to ${opts.output}`));
|
|
98088
98254
|
}
|
|
98089
98255
|
if (opts.json) {
|
|
@@ -98192,7 +98358,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
98192
98358
|
if (opts.json || opts.output) {
|
|
98193
98359
|
const jsonOutput = formatJSON(run2, results);
|
|
98194
98360
|
if (opts.output) {
|
|
98195
|
-
|
|
98361
|
+
writeFileSync7(opts.output, jsonOutput, "utf-8");
|
|
98196
98362
|
log(chalk6.green(`Results written to ${opts.output}`));
|
|
98197
98363
|
}
|
|
98198
98364
|
if (opts.json) {
|
|
@@ -98445,13 +98611,13 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
|
|
|
98445
98611
|
if (fmt === "json") {
|
|
98446
98612
|
const outputPath = opts.output ?? "testers-export.json";
|
|
98447
98613
|
const data = JSON.stringify(scenarios, null, 2);
|
|
98448
|
-
|
|
98614
|
+
writeFileSync7(outputPath, data, "utf-8");
|
|
98449
98615
|
log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${resolve4(outputPath)}`));
|
|
98450
98616
|
return;
|
|
98451
98617
|
}
|
|
98452
98618
|
const outputDir = opts.output ?? ".";
|
|
98453
|
-
if (!
|
|
98454
|
-
|
|
98619
|
+
if (!existsSync18(outputDir)) {
|
|
98620
|
+
mkdirSync15(outputDir, { recursive: true });
|
|
98455
98621
|
}
|
|
98456
98622
|
for (const s2 of scenarios) {
|
|
98457
98623
|
const lines = [];
|
|
@@ -98479,7 +98645,7 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
|
|
|
98479
98645
|
}
|
|
98480
98646
|
const safeFilename = s2.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
|
|
98481
98647
|
const filePath = join20(outputDir, `${s2.shortId}-${safeFilename}.md`);
|
|
98482
|
-
|
|
98648
|
+
writeFileSync7(filePath, lines.join(`
|
|
98483
98649
|
`), "utf-8");
|
|
98484
98650
|
log(chalk6.dim(` ${s2.shortId}: ${s2.name} \u2192 ${filePath}`));
|
|
98485
98651
|
}
|
|
@@ -98653,17 +98819,17 @@ projectCmd.command("export-open <id>").description("Register a testers project i
|
|
|
98653
98819
|
projectCmd.command("use <name>").description("Set active project (find or create)").option("--json", "Output as JSON", false).action((name21, opts) => {
|
|
98654
98820
|
try {
|
|
98655
98821
|
const project = ensureProject(name21, process.cwd());
|
|
98656
|
-
if (!
|
|
98657
|
-
|
|
98822
|
+
if (!existsSync18(CONFIG_DIR5)) {
|
|
98823
|
+
mkdirSync15(CONFIG_DIR5, { recursive: true });
|
|
98658
98824
|
}
|
|
98659
98825
|
let config2 = {};
|
|
98660
|
-
if (
|
|
98826
|
+
if (existsSync18(CONFIG_PATH4)) {
|
|
98661
98827
|
try {
|
|
98662
98828
|
config2 = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
|
|
98663
98829
|
} catch {}
|
|
98664
98830
|
}
|
|
98665
98831
|
config2.activeProject = project.id;
|
|
98666
|
-
|
|
98832
|
+
writeFileSync7(CONFIG_PATH4, JSON.stringify(config2, null, 2), "utf-8");
|
|
98667
98833
|
if (opts.json) {
|
|
98668
98834
|
log(JSON.stringify({ activeProject: project.id, project }, null, 2));
|
|
98669
98835
|
return;
|
|
@@ -99272,10 +99438,10 @@ program2.command("ci [provider]").description("Print or write a CI workflow (def
|
|
|
99272
99438
|
if (opts.output) {
|
|
99273
99439
|
const outPath = resolve4(opts.output);
|
|
99274
99440
|
const outDir = outPath.replace(/\/[^/]*$/, "");
|
|
99275
|
-
if (outDir && !
|
|
99276
|
-
|
|
99441
|
+
if (outDir && !existsSync18(outDir)) {
|
|
99442
|
+
mkdirSync15(outDir, { recursive: true });
|
|
99277
99443
|
}
|
|
99278
|
-
|
|
99444
|
+
writeFileSync7(outPath, workflow, "utf-8");
|
|
99279
99445
|
log(chalk6.green(`Workflow written to ${outPath}`));
|
|
99280
99446
|
return;
|
|
99281
99447
|
}
|
|
@@ -99307,11 +99473,11 @@ program2.command("init").description("Initialize a new testing project").option(
|
|
|
99307
99473
|
}
|
|
99308
99474
|
if (opts.ci === "github") {
|
|
99309
99475
|
const workflowDir = join20(process.cwd(), ".github", "workflows");
|
|
99310
|
-
if (!
|
|
99311
|
-
|
|
99476
|
+
if (!existsSync18(workflowDir)) {
|
|
99477
|
+
mkdirSync15(workflowDir, { recursive: true });
|
|
99312
99478
|
}
|
|
99313
99479
|
const workflowPath = join20(workflowDir, "testers.yml");
|
|
99314
|
-
|
|
99480
|
+
writeFileSync7(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
|
|
99315
99481
|
log(` CI: ${chalk6.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
|
|
99316
99482
|
} else if (opts.ci) {
|
|
99317
99483
|
log(chalk6.yellow(` Unknown CI provider: ${opts.ci}. Supported: github`));
|
|
@@ -99504,7 +99670,7 @@ program2.command("quick-qa <url>").alias("quick-check").description("Run a fast
|
|
|
99504
99670
|
wcagLevel
|
|
99505
99671
|
});
|
|
99506
99672
|
if (opts.output) {
|
|
99507
|
-
|
|
99673
|
+
writeFileSync7(resolve4(opts.output), JSON.stringify(result, null, 2));
|
|
99508
99674
|
}
|
|
99509
99675
|
if (opts.json) {
|
|
99510
99676
|
log(JSON.stringify(result, null, 2));
|
|
@@ -99555,7 +99721,7 @@ program2.command("report [run-id]").description("Generate HTML test report or co
|
|
|
99555
99721
|
format
|
|
99556
99722
|
});
|
|
99557
99723
|
if (opts.output && opts.output !== "report.html") {
|
|
99558
|
-
|
|
99724
|
+
writeFileSync7(opts.output, content, "utf-8");
|
|
99559
99725
|
const absPath2 = resolve4(opts.output);
|
|
99560
99726
|
log(chalk6.green(`Compliance report written to ${absPath2}`));
|
|
99561
99727
|
} else {
|
|
@@ -99569,7 +99735,7 @@ program2.command("report [run-id]").description("Generate HTML test report or co
|
|
|
99569
99735
|
} else {
|
|
99570
99736
|
html = generateHtmlReport(runId);
|
|
99571
99737
|
}
|
|
99572
|
-
|
|
99738
|
+
writeFileSync7(opts.output, html, "utf-8");
|
|
99573
99739
|
const absPath = resolve4(opts.output);
|
|
99574
99740
|
log(chalk6.green(`Report generated: ${absPath}`));
|
|
99575
99741
|
if (opts.open) {
|
|
@@ -99582,12 +99748,22 @@ program2.command("report [run-id]").description("Generate HTML test report or co
|
|
|
99582
99748
|
}
|
|
99583
99749
|
});
|
|
99584
99750
|
var authCmd = program2.command("auth").description("Manage auth presets");
|
|
99585
|
-
authCmd.command("add <name>").description("Create an auth preset").
|
|
99751
|
+
authCmd.command("add <name>").description("Create an auth preset").option("--email <email>", "Login email or credential reference").option("--password <password>", "Login password or credential reference").option("--email-env <name>", "Environment variable name for the login email").option("--password-env <name>", "Environment variable name for the login password").option("--login-path <path>", "Login page path", "/login").action((name21, opts) => {
|
|
99586
99752
|
try {
|
|
99753
|
+
const email3 = opts.email ?? envCredentialRef(opts.emailEnv);
|
|
99754
|
+
const password = opts.password ?? envCredentialRef(opts.passwordEnv);
|
|
99755
|
+
if (!email3) {
|
|
99756
|
+
logError(chalk6.red("Error: provide --email or --email-env"));
|
|
99757
|
+
process.exit(1);
|
|
99758
|
+
}
|
|
99759
|
+
if (!password) {
|
|
99760
|
+
logError(chalk6.red("Error: provide --password or --password-env"));
|
|
99761
|
+
process.exit(1);
|
|
99762
|
+
}
|
|
99587
99763
|
const preset = createAuthPreset({
|
|
99588
99764
|
name: name21,
|
|
99589
|
-
email:
|
|
99590
|
-
password
|
|
99765
|
+
email: email3,
|
|
99766
|
+
password,
|
|
99591
99767
|
loginPath: opts.loginPath
|
|
99592
99768
|
});
|
|
99593
99769
|
log(chalk6.green(`Created auth preset ${chalk6.bold(preset.name)} (${preset.email})`));
|
|
@@ -100849,7 +101025,10 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
|
|
|
100849
101025
|
}, []).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) => {
|
|
100850
101026
|
acc.push(val);
|
|
100851
101027
|
return acc;
|
|
100852
|
-
}, []).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("--
|
|
101028
|
+
}, []).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-env <assignment>", "Sandbox env var; KEY forwards host KEY, KEY=value stores value (repeatable)", (val, acc) => {
|
|
101029
|
+
acc.push(val);
|
|
101030
|
+
return acc;
|
|
101031
|
+
}, []).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) => {
|
|
100853
101032
|
try {
|
|
100854
101033
|
const workflow = createTestingWorkflow({
|
|
100855
101034
|
name: name21,
|
|
@@ -100875,6 +101054,13 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
|
|
|
100875
101054
|
sandboxSyncStrategy: opts.sandboxSync,
|
|
100876
101055
|
setupCommand: opts.sandboxSetupCommand,
|
|
100877
101056
|
packageSpec: opts.sandboxPackage,
|
|
101057
|
+
env: parseSandboxEnv(opts.sandboxEnv),
|
|
101058
|
+
appSourceDir: opts.sandboxAppSource,
|
|
101059
|
+
appRemoteDir: opts.sandboxAppRemoteDir,
|
|
101060
|
+
appStartCommand: opts.sandboxAppStartCommand,
|
|
101061
|
+
appUrl: opts.sandboxAppUrl,
|
|
101062
|
+
appWaitUrl: opts.sandboxAppWaitUrl,
|
|
101063
|
+
appWaitTimeoutMs: opts.sandboxAppWaitTimeout ? parseInt(opts.sandboxAppWaitTimeout, 10) : undefined,
|
|
100878
101064
|
timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined
|
|
100879
101065
|
}
|
|
100880
101066
|
});
|