@hasna/testers 0.0.45 → 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 +192 -67
- 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 ===
|
|
@@ -97519,7 +97638,7 @@ program2.command("prod-debug <target>").description("Create a safe production de
|
|
|
97519
97638
|
}, config2.prodDebug);
|
|
97520
97639
|
const output = opts.json ? JSON.stringify(plan, null, 2) : formatProdDebugPlan(plan);
|
|
97521
97640
|
if (opts.output) {
|
|
97522
|
-
|
|
97641
|
+
writeFileSync7(resolve4(opts.output), output + `
|
|
97523
97642
|
`);
|
|
97524
97643
|
} else {
|
|
97525
97644
|
log(output);
|
|
@@ -97529,7 +97648,7 @@ var CONFIG_DIR5 = getTestersDir();
|
|
|
97529
97648
|
var CONFIG_PATH4 = join20(CONFIG_DIR5, "config.json");
|
|
97530
97649
|
function getActiveProject() {
|
|
97531
97650
|
try {
|
|
97532
|
-
if (
|
|
97651
|
+
if (existsSync18(CONFIG_PATH4)) {
|
|
97533
97652
|
const raw = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
|
|
97534
97653
|
return raw.activeProject ?? undefined;
|
|
97535
97654
|
}
|
|
@@ -98083,7 +98202,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
98083
98202
|
if (opts.json || opts.output) {
|
|
98084
98203
|
const jsonOutput = formatJSON(run3, results2);
|
|
98085
98204
|
if (opts.output) {
|
|
98086
|
-
|
|
98205
|
+
writeFileSync7(opts.output, jsonOutput, "utf-8");
|
|
98087
98206
|
log(chalk6.green(`Results written to ${opts.output}`));
|
|
98088
98207
|
}
|
|
98089
98208
|
if (opts.json) {
|
|
@@ -98192,7 +98311,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
98192
98311
|
if (opts.json || opts.output) {
|
|
98193
98312
|
const jsonOutput = formatJSON(run2, results);
|
|
98194
98313
|
if (opts.output) {
|
|
98195
|
-
|
|
98314
|
+
writeFileSync7(opts.output, jsonOutput, "utf-8");
|
|
98196
98315
|
log(chalk6.green(`Results written to ${opts.output}`));
|
|
98197
98316
|
}
|
|
98198
98317
|
if (opts.json) {
|
|
@@ -98445,13 +98564,13 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
|
|
|
98445
98564
|
if (fmt === "json") {
|
|
98446
98565
|
const outputPath = opts.output ?? "testers-export.json";
|
|
98447
98566
|
const data = JSON.stringify(scenarios, null, 2);
|
|
98448
|
-
|
|
98567
|
+
writeFileSync7(outputPath, data, "utf-8");
|
|
98449
98568
|
log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${resolve4(outputPath)}`));
|
|
98450
98569
|
return;
|
|
98451
98570
|
}
|
|
98452
98571
|
const outputDir = opts.output ?? ".";
|
|
98453
|
-
if (!
|
|
98454
|
-
|
|
98572
|
+
if (!existsSync18(outputDir)) {
|
|
98573
|
+
mkdirSync15(outputDir, { recursive: true });
|
|
98455
98574
|
}
|
|
98456
98575
|
for (const s2 of scenarios) {
|
|
98457
98576
|
const lines = [];
|
|
@@ -98479,7 +98598,7 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
|
|
|
98479
98598
|
}
|
|
98480
98599
|
const safeFilename = s2.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
|
|
98481
98600
|
const filePath = join20(outputDir, `${s2.shortId}-${safeFilename}.md`);
|
|
98482
|
-
|
|
98601
|
+
writeFileSync7(filePath, lines.join(`
|
|
98483
98602
|
`), "utf-8");
|
|
98484
98603
|
log(chalk6.dim(` ${s2.shortId}: ${s2.name} \u2192 ${filePath}`));
|
|
98485
98604
|
}
|
|
@@ -98653,17 +98772,17 @@ projectCmd.command("export-open <id>").description("Register a testers project i
|
|
|
98653
98772
|
projectCmd.command("use <name>").description("Set active project (find or create)").option("--json", "Output as JSON", false).action((name21, opts) => {
|
|
98654
98773
|
try {
|
|
98655
98774
|
const project = ensureProject(name21, process.cwd());
|
|
98656
|
-
if (!
|
|
98657
|
-
|
|
98775
|
+
if (!existsSync18(CONFIG_DIR5)) {
|
|
98776
|
+
mkdirSync15(CONFIG_DIR5, { recursive: true });
|
|
98658
98777
|
}
|
|
98659
98778
|
let config2 = {};
|
|
98660
|
-
if (
|
|
98779
|
+
if (existsSync18(CONFIG_PATH4)) {
|
|
98661
98780
|
try {
|
|
98662
98781
|
config2 = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
|
|
98663
98782
|
} catch {}
|
|
98664
98783
|
}
|
|
98665
98784
|
config2.activeProject = project.id;
|
|
98666
|
-
|
|
98785
|
+
writeFileSync7(CONFIG_PATH4, JSON.stringify(config2, null, 2), "utf-8");
|
|
98667
98786
|
if (opts.json) {
|
|
98668
98787
|
log(JSON.stringify({ activeProject: project.id, project }, null, 2));
|
|
98669
98788
|
return;
|
|
@@ -99272,10 +99391,10 @@ program2.command("ci [provider]").description("Print or write a CI workflow (def
|
|
|
99272
99391
|
if (opts.output) {
|
|
99273
99392
|
const outPath = resolve4(opts.output);
|
|
99274
99393
|
const outDir = outPath.replace(/\/[^/]*$/, "");
|
|
99275
|
-
if (outDir && !
|
|
99276
|
-
|
|
99394
|
+
if (outDir && !existsSync18(outDir)) {
|
|
99395
|
+
mkdirSync15(outDir, { recursive: true });
|
|
99277
99396
|
}
|
|
99278
|
-
|
|
99397
|
+
writeFileSync7(outPath, workflow, "utf-8");
|
|
99279
99398
|
log(chalk6.green(`Workflow written to ${outPath}`));
|
|
99280
99399
|
return;
|
|
99281
99400
|
}
|
|
@@ -99307,11 +99426,11 @@ program2.command("init").description("Initialize a new testing project").option(
|
|
|
99307
99426
|
}
|
|
99308
99427
|
if (opts.ci === "github") {
|
|
99309
99428
|
const workflowDir = join20(process.cwd(), ".github", "workflows");
|
|
99310
|
-
if (!
|
|
99311
|
-
|
|
99429
|
+
if (!existsSync18(workflowDir)) {
|
|
99430
|
+
mkdirSync15(workflowDir, { recursive: true });
|
|
99312
99431
|
}
|
|
99313
99432
|
const workflowPath = join20(workflowDir, "testers.yml");
|
|
99314
|
-
|
|
99433
|
+
writeFileSync7(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
|
|
99315
99434
|
log(` CI: ${chalk6.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
|
|
99316
99435
|
} else if (opts.ci) {
|
|
99317
99436
|
log(chalk6.yellow(` Unknown CI provider: ${opts.ci}. Supported: github`));
|
|
@@ -99504,7 +99623,7 @@ program2.command("quick-qa <url>").alias("quick-check").description("Run a fast
|
|
|
99504
99623
|
wcagLevel
|
|
99505
99624
|
});
|
|
99506
99625
|
if (opts.output) {
|
|
99507
|
-
|
|
99626
|
+
writeFileSync7(resolve4(opts.output), JSON.stringify(result, null, 2));
|
|
99508
99627
|
}
|
|
99509
99628
|
if (opts.json) {
|
|
99510
99629
|
log(JSON.stringify(result, null, 2));
|
|
@@ -99555,7 +99674,7 @@ program2.command("report [run-id]").description("Generate HTML test report or co
|
|
|
99555
99674
|
format
|
|
99556
99675
|
});
|
|
99557
99676
|
if (opts.output && opts.output !== "report.html") {
|
|
99558
|
-
|
|
99677
|
+
writeFileSync7(opts.output, content, "utf-8");
|
|
99559
99678
|
const absPath2 = resolve4(opts.output);
|
|
99560
99679
|
log(chalk6.green(`Compliance report written to ${absPath2}`));
|
|
99561
99680
|
} else {
|
|
@@ -99569,7 +99688,7 @@ program2.command("report [run-id]").description("Generate HTML test report or co
|
|
|
99569
99688
|
} else {
|
|
99570
99689
|
html = generateHtmlReport(runId);
|
|
99571
99690
|
}
|
|
99572
|
-
|
|
99691
|
+
writeFileSync7(opts.output, html, "utf-8");
|
|
99573
99692
|
const absPath = resolve4(opts.output);
|
|
99574
99693
|
log(chalk6.green(`Report generated: ${absPath}`));
|
|
99575
99694
|
if (opts.open) {
|
|
@@ -100849,7 +100968,7 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
|
|
|
100849
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) => {
|
|
100850
100969
|
acc.push(val);
|
|
100851
100970
|
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("--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) => {
|
|
100853
100972
|
try {
|
|
100854
100973
|
const workflow = createTestingWorkflow({
|
|
100855
100974
|
name: name21,
|
|
@@ -100875,6 +100994,12 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
|
|
|
100875
100994
|
sandboxSyncStrategy: opts.sandboxSync,
|
|
100876
100995
|
setupCommand: opts.sandboxSetupCommand,
|
|
100877
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,
|
|
100878
101003
|
timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined
|
|
100879
101004
|
}
|
|
100880
101005
|
});
|