@hasna/testers 0.0.50 → 0.0.52
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/README.md +1 -1
- package/dist/cli/index.js +418 -36
- package/dist/db/scenarios.d.ts.map +1 -1
- package/dist/index.js +180 -24
- package/dist/lib/ai-client.d.ts.map +1 -1
- package/dist/lib/next-route-inventory.d.ts.map +1 -1
- package/dist/lib/route-fixtures.d.ts +20 -0
- package/dist/lib/route-fixtures.d.ts.map +1 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/workflow-fanout.d.ts +26 -0
- package/dist/lib/workflow-fanout.d.ts.map +1 -1
- package/dist/mcp/index.js +181 -25
- package/dist/server/index.js +181 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ testers workflow fanout --project alumia --workers 6 --url https://preview.examp
|
|
|
41
41
|
testers workflow fanout wf_abc,wf_def wf_xyz --workers 12 --url https://preview.example.com --json
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
`--workers` is bounded to 1-12 concurrent sandboxes. Use `--dry-run` to inspect the remote commands
|
|
44
|
+
`--workers` is bounded to 1-12 concurrent sandboxes. Fanout preflights provider credentials, required sandbox environment references, `rsync`, and app source directories before launching workers. Use `--dry-run` to inspect the remote commands, upload plans, and preflight checks without spawning sandboxes.
|
|
45
45
|
|
|
46
46
|
### Common Flags
|
|
47
47
|
|
package/dist/cli/index.js
CHANGED
|
@@ -12394,6 +12394,18 @@ __export(exports_scenarios, {
|
|
|
12394
12394
|
createScenario: () => createScenario,
|
|
12395
12395
|
countScenarios: () => countScenarios
|
|
12396
12396
|
});
|
|
12397
|
+
function stableJson(value) {
|
|
12398
|
+
if (value === undefined)
|
|
12399
|
+
return "";
|
|
12400
|
+
if (value === null)
|
|
12401
|
+
return "null";
|
|
12402
|
+
if (Array.isArray(value))
|
|
12403
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
12404
|
+
if (typeof value === "object") {
|
|
12405
|
+
return `{${Object.entries(value).filter(([, val]) => val !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, val]) => `${JSON.stringify(key)}:${stableJson(val)}`).join(",")}}`;
|
|
12406
|
+
}
|
|
12407
|
+
return JSON.stringify(value);
|
|
12408
|
+
}
|
|
12397
12409
|
function nextShortId(projectId) {
|
|
12398
12410
|
const db2 = getDatabase();
|
|
12399
12411
|
if (projectId) {
|
|
@@ -12653,9 +12665,12 @@ function upsertScenario(input) {
|
|
|
12653
12665
|
}
|
|
12654
12666
|
const existingSteps = JSON.parse(existing.steps);
|
|
12655
12667
|
const existingTags = JSON.parse(existing.tags);
|
|
12668
|
+
const existingMetadata = existing.metadata ? JSON.parse(existing.metadata) : undefined;
|
|
12669
|
+
const existingAssertions = JSON.parse(existing.assertions || "[]");
|
|
12670
|
+
const existingParameters = existing.parameters ? JSON.parse(existing.parameters) : undefined;
|
|
12656
12671
|
const newSteps = input.steps ?? [];
|
|
12657
12672
|
const newTags = input.tags ?? [];
|
|
12658
|
-
const isIdentical = existing.description === (input.description ?? "") && existingSteps.length === newSteps.length && existingSteps.every((s, i) => s === newSteps[i]) && existingTags.length === newTags.length && existingTags.every((t, i) => t === newTags[i]) && existing.priority === (input.priority ?? "medium");
|
|
12673
|
+
const isIdentical = existing.description === (input.description ?? "") && existingSteps.length === newSteps.length && existingSteps.every((s, i) => s === newSteps[i]) && existingTags.length === newTags.length && existingTags.every((t, i) => t === newTags[i]) && existing.priority === (input.priority ?? "medium") && existing.target_path === (input.targetPath ?? null) && Boolean(existing.requires_auth) === Boolean(input.requiresAuth) && stableJson(existingMetadata) === stableJson(input.metadata) && stableJson(existingAssertions) === stableJson(input.assertions ?? []) && stableJson(existingParameters) === stableJson(input.parameters);
|
|
12659
12674
|
if (isIdentical) {
|
|
12660
12675
|
return { scenario: scenarioFromRow(existing), action: "deduped" };
|
|
12661
12676
|
}
|
|
@@ -12705,6 +12720,10 @@ function upsertScenario(input) {
|
|
|
12705
12720
|
sets.push("assertions = ?");
|
|
12706
12721
|
params.push(JSON.stringify(input.assertions));
|
|
12707
12722
|
}
|
|
12723
|
+
if (input.parameters !== undefined) {
|
|
12724
|
+
sets.push("parameters = ?");
|
|
12725
|
+
params.push(JSON.stringify(input.parameters));
|
|
12726
|
+
}
|
|
12708
12727
|
sets.push("version = ?", "updated_at = ?");
|
|
12709
12728
|
params.push(existing.version + 1, now());
|
|
12710
12729
|
params.push(existing.id);
|
|
@@ -13057,6 +13076,159 @@ var init_screenshots = __esm(() => {
|
|
|
13057
13076
|
init_database();
|
|
13058
13077
|
});
|
|
13059
13078
|
|
|
13079
|
+
// src/lib/route-fixtures.ts
|
|
13080
|
+
function isRecord2(value) {
|
|
13081
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13082
|
+
}
|
|
13083
|
+
function envNameForParam(prefix, param) {
|
|
13084
|
+
return `${prefix}_${param.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}`;
|
|
13085
|
+
}
|
|
13086
|
+
function readString(value) {
|
|
13087
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
13088
|
+
}
|
|
13089
|
+
function resolveReference(value, env) {
|
|
13090
|
+
if (value.startsWith("$?"))
|
|
13091
|
+
return env[value.slice(2)]?.trim() || undefined;
|
|
13092
|
+
if (value.startsWith("$"))
|
|
13093
|
+
return env[value.slice(1)]?.trim() || undefined;
|
|
13094
|
+
return value;
|
|
13095
|
+
}
|
|
13096
|
+
function scenarioFixtureValue(params, name, env) {
|
|
13097
|
+
if (!params)
|
|
13098
|
+
return;
|
|
13099
|
+
const routeFixtures = isRecord2(params["routeFixtures"]) ? params["routeFixtures"] : {};
|
|
13100
|
+
const raw = readString(routeFixtures[name]) ?? readString(params[name]);
|
|
13101
|
+
return raw ? resolveReference(raw, env) : undefined;
|
|
13102
|
+
}
|
|
13103
|
+
function envFixtureValue(name, env) {
|
|
13104
|
+
const candidates = [
|
|
13105
|
+
envNameForParam("TESTERS_ROUTE", name),
|
|
13106
|
+
envNameForParam("TESTERS_FIXTURE", name),
|
|
13107
|
+
envNameForParam("ALUMIA_FIXTURE", name),
|
|
13108
|
+
...PARAM_ENV_CANDIDATES[name] ?? []
|
|
13109
|
+
];
|
|
13110
|
+
for (const candidate of candidates) {
|
|
13111
|
+
const value = env[candidate]?.trim();
|
|
13112
|
+
if (value)
|
|
13113
|
+
return value;
|
|
13114
|
+
}
|
|
13115
|
+
return;
|
|
13116
|
+
}
|
|
13117
|
+
function defaultFixtureValue(name) {
|
|
13118
|
+
if (name === "orgSlug")
|
|
13119
|
+
return "test-org";
|
|
13120
|
+
if (name.toLowerCase().endsWith("slug")) {
|
|
13121
|
+
return `test-${name.replace(/Slug$/i, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase() || "slug"}`;
|
|
13122
|
+
}
|
|
13123
|
+
if (name === "id" || name.toLowerCase().endsWith("id"))
|
|
13124
|
+
return DEFAULT_UUID;
|
|
13125
|
+
if (name.toLowerCase().includes("token"))
|
|
13126
|
+
return "test-token";
|
|
13127
|
+
return `test-${name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()}`;
|
|
13128
|
+
}
|
|
13129
|
+
function routeParamsFromPath(path) {
|
|
13130
|
+
if (!path)
|
|
13131
|
+
return [];
|
|
13132
|
+
const params = new Set;
|
|
13133
|
+
for (const match of path.matchAll(/:([A-Za-z0-9_]+)(?:\*\??)?/g)) {
|
|
13134
|
+
if (match[1])
|
|
13135
|
+
params.add(match[1]);
|
|
13136
|
+
}
|
|
13137
|
+
return [...params];
|
|
13138
|
+
}
|
|
13139
|
+
function defaultRouteFixturesForParams(params) {
|
|
13140
|
+
return Object.fromEntries(params.map((param) => [param, defaultFixtureValue(param)]));
|
|
13141
|
+
}
|
|
13142
|
+
function resolveRouteFixtures(scenario, env = process.env) {
|
|
13143
|
+
const metadataParams = Array.isArray(scenario.metadata?.["fixtureParams"]) ? scenario.metadata["fixtureParams"].filter((value) => typeof value === "string") : [];
|
|
13144
|
+
const params = [...new Set([...metadataParams, ...routeParamsFromPath(scenario.targetPath)])];
|
|
13145
|
+
const values = {};
|
|
13146
|
+
const sources = {};
|
|
13147
|
+
const synthetic = [];
|
|
13148
|
+
for (const param of params) {
|
|
13149
|
+
const scenarioValue = scenarioFixtureValue(scenario.parameters, param, env);
|
|
13150
|
+
if (scenarioValue) {
|
|
13151
|
+
values[param] = scenarioValue;
|
|
13152
|
+
sources[param] = "scenario";
|
|
13153
|
+
continue;
|
|
13154
|
+
}
|
|
13155
|
+
const envValue = envFixtureValue(param, env);
|
|
13156
|
+
if (envValue) {
|
|
13157
|
+
values[param] = envValue;
|
|
13158
|
+
sources[param] = "env";
|
|
13159
|
+
continue;
|
|
13160
|
+
}
|
|
13161
|
+
values[param] = defaultFixtureValue(param);
|
|
13162
|
+
sources[param] = "default";
|
|
13163
|
+
synthetic.push(param);
|
|
13164
|
+
}
|
|
13165
|
+
const resolvedPath = scenario.targetPath ? resolveRoutePath(scenario.targetPath, values) : null;
|
|
13166
|
+
return {
|
|
13167
|
+
originalPath: scenario.targetPath,
|
|
13168
|
+
resolvedPath,
|
|
13169
|
+
params,
|
|
13170
|
+
values,
|
|
13171
|
+
sources,
|
|
13172
|
+
synthetic
|
|
13173
|
+
};
|
|
13174
|
+
}
|
|
13175
|
+
function resolveRoutePath(path, values) {
|
|
13176
|
+
return path.replace(/\/:([A-Za-z0-9_]+)\*\?/g, (_match, name) => {
|
|
13177
|
+
const value = values[name];
|
|
13178
|
+
return value ? `/${encodeRouteFixture(value, true)}` : "";
|
|
13179
|
+
}).replace(/:([A-Za-z0-9_]+)\*/g, (_match, name) => encodeRouteFixture(values[name] ?? defaultFixtureValue(name), true)).replace(/:([A-Za-z0-9_]+)/g, (_match, name) => encodeRouteFixture(values[name] ?? defaultFixtureValue(name), false));
|
|
13180
|
+
}
|
|
13181
|
+
function encodeRouteFixture(value, allowSlash) {
|
|
13182
|
+
if (allowSlash)
|
|
13183
|
+
return value.split("/").filter(Boolean).map(encodeURIComponent).join("/");
|
|
13184
|
+
return encodeURIComponent(value);
|
|
13185
|
+
}
|
|
13186
|
+
function materializeScenarioRoute(scenario, env = process.env) {
|
|
13187
|
+
const resolution = resolveRouteFixtures(scenario, env);
|
|
13188
|
+
if (!resolution.resolvedPath || resolution.resolvedPath === scenario.targetPath) {
|
|
13189
|
+
return { scenario, resolution };
|
|
13190
|
+
}
|
|
13191
|
+
const steps = scenario.steps.map((step) => {
|
|
13192
|
+
let next = step;
|
|
13193
|
+
for (const [name, value] of Object.entries(resolution.values)) {
|
|
13194
|
+
next = next.replaceAll(`:${name}`, value).replaceAll(`[${name}]`, value).replaceAll(`{${name}}`, value);
|
|
13195
|
+
}
|
|
13196
|
+
return next.replaceAll(scenario.targetPath ?? "", resolution.resolvedPath ?? "");
|
|
13197
|
+
});
|
|
13198
|
+
return {
|
|
13199
|
+
scenario: {
|
|
13200
|
+
...scenario,
|
|
13201
|
+
targetPath: resolution.resolvedPath,
|
|
13202
|
+
steps,
|
|
13203
|
+
metadata: {
|
|
13204
|
+
...scenario.metadata ?? {},
|
|
13205
|
+
routeFixtureResolution: resolution
|
|
13206
|
+
}
|
|
13207
|
+
},
|
|
13208
|
+
resolution
|
|
13209
|
+
};
|
|
13210
|
+
}
|
|
13211
|
+
function resolveStartUrl(baseUrl, targetPath) {
|
|
13212
|
+
try {
|
|
13213
|
+
return new URL(targetPath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
13214
|
+
} catch {
|
|
13215
|
+
return `${baseUrl.replace(/\/+$/, "")}/${targetPath.replace(/^\/+/, "")}`;
|
|
13216
|
+
}
|
|
13217
|
+
}
|
|
13218
|
+
var DEFAULT_UUID = "00000000-0000-4000-8000-000000000000", PARAM_ENV_CANDIDATES;
|
|
13219
|
+
var init_route_fixtures = __esm(() => {
|
|
13220
|
+
PARAM_ENV_CANDIDATES = {
|
|
13221
|
+
orgSlug: ["TESTERS_ORG_SLUG", "SMOKE_ORG_SLUG", "ORG_SLUG"],
|
|
13222
|
+
orgId: ["TESTERS_ORG_ID", "SMOKE_ORG_ID", "ORG_ID"],
|
|
13223
|
+
projectSlug: ["TESTERS_PROJECT_SLUG", "SMOKE_PROJECT_SLUG", "PROJECT_SLUG"],
|
|
13224
|
+
projectId: ["TESTERS_PROJECT_ID", "SMOKE_PROJECT_ID", "PROJECT_ID"],
|
|
13225
|
+
workspaceId: ["TESTERS_WORKSPACE_ID", "SMOKE_WORKSPACE_ID", "WORKSPACE_ID"],
|
|
13226
|
+
agentId: ["TESTERS_AGENT_ID", "SMOKE_AGENT_ID", "AGENT_ID"],
|
|
13227
|
+
sessionId: ["TESTERS_SESSION_ID", "SMOKE_SESSION_ID", "SESSION_ID"],
|
|
13228
|
+
userId: ["TESTERS_USER_ID", "SMOKE_USER_ID", "USER_ID"]
|
|
13229
|
+
};
|
|
13230
|
+
});
|
|
13231
|
+
|
|
13060
13232
|
// src/lib/browser-lightpanda.ts
|
|
13061
13233
|
var exports_browser_lightpanda = {};
|
|
13062
13234
|
__export(exports_browser_lightpanda, {
|
|
@@ -14777,33 +14949,35 @@ ${filtered.join(`
|
|
|
14777
14949
|
return { result: `Error executing ${toolName}: ${message}` };
|
|
14778
14950
|
}
|
|
14779
14951
|
}
|
|
14780
|
-
function resolveStartUrl(baseUrl, targetPath) {
|
|
14781
|
-
try {
|
|
14782
|
-
return new URL(targetPath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
14783
|
-
} catch {
|
|
14784
|
-
return `${baseUrl.replace(/\/+$/, "")}/${targetPath.replace(/^\/+/, "")}`;
|
|
14785
|
-
}
|
|
14786
|
-
}
|
|
14787
14952
|
function buildScenarioUserMessage(scenario, baseUrl) {
|
|
14953
|
+
const { scenario: materializedScenario, resolution } = materializeScenarioRoute(scenario);
|
|
14788
14954
|
const userParts = [
|
|
14789
|
-
`**Scenario:** ${
|
|
14790
|
-
`**Description:** ${
|
|
14955
|
+
`**Scenario:** ${materializedScenario.name}`,
|
|
14956
|
+
`**Description:** ${materializedScenario.description}`
|
|
14791
14957
|
];
|
|
14792
14958
|
if (baseUrl) {
|
|
14793
14959
|
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
14794
14960
|
userParts.push(`**Base URL:** ${normalizedBaseUrl}`);
|
|
14795
|
-
if (
|
|
14796
|
-
userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl,
|
|
14961
|
+
if (materializedScenario.targetPath) {
|
|
14962
|
+
userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl, materializedScenario.targetPath)}`);
|
|
14797
14963
|
}
|
|
14798
14964
|
userParts.push("**Navigation Boundary:** Treat the Base URL as the application under test. Resolve relative paths and in-app navigation against this origin. Do not navigate to another host unless a step explicitly includes an absolute external URL.");
|
|
14799
14965
|
}
|
|
14800
|
-
if (
|
|
14801
|
-
userParts.push(`**Target Path:** ${
|
|
14966
|
+
if (materializedScenario.targetPath) {
|
|
14967
|
+
userParts.push(`**Target Path:** ${materializedScenario.targetPath}`);
|
|
14802
14968
|
}
|
|
14803
|
-
if (
|
|
14969
|
+
if (resolution.params.length > 0) {
|
|
14970
|
+
userParts.push("**Route Fixtures:**");
|
|
14971
|
+
for (const param of resolution.params) {
|
|
14972
|
+
const source = resolution.sources[param];
|
|
14973
|
+
const synthetic = source === "default" ? " synthetic" : "";
|
|
14974
|
+
userParts.push(`- :${param} = ${resolution.values[param]} (${source}${synthetic})`);
|
|
14975
|
+
}
|
|
14976
|
+
}
|
|
14977
|
+
if (materializedScenario.steps.length > 0) {
|
|
14804
14978
|
userParts.push("**Steps:**");
|
|
14805
|
-
for (let i = 0;i <
|
|
14806
|
-
userParts.push(`${i + 1}. ${
|
|
14979
|
+
for (let i = 0;i < materializedScenario.steps.length; i++) {
|
|
14980
|
+
userParts.push(`${i + 1}. ${materializedScenario.steps[i]}`);
|
|
14807
14981
|
}
|
|
14808
14982
|
}
|
|
14809
14983
|
return userParts.join(`
|
|
@@ -15101,6 +15275,7 @@ function createClientForModel(model, apiKey) {
|
|
|
15101
15275
|
var activeHARs, activeCoverage, BROWSER_TOOLS;
|
|
15102
15276
|
var init_ai_client = __esm(() => {
|
|
15103
15277
|
init_types();
|
|
15278
|
+
init_route_fixtures();
|
|
15104
15279
|
activeHARs = new Map;
|
|
15105
15280
|
activeCoverage = new Map;
|
|
15106
15281
|
BROWSER_TOOLS = [
|
|
@@ -18321,9 +18496,10 @@ function withTimeout(promise, ms, label) {
|
|
|
18321
18496
|
});
|
|
18322
18497
|
}
|
|
18323
18498
|
async function runSingleScenario(scenario, runId, options) {
|
|
18324
|
-
const
|
|
18499
|
+
const { scenario: materializedScenario, resolution: routeFixtureResolution } = materializeScenarioRoute(scenario);
|
|
18500
|
+
const scenarioType = materializedScenario.scenarioType ?? "browser";
|
|
18325
18501
|
if (scenarioType === "eval") {
|
|
18326
|
-
return runEvalScenario(
|
|
18502
|
+
return runEvalScenario(materializedScenario, { runId, baseUrl: options.url });
|
|
18327
18503
|
}
|
|
18328
18504
|
const config = loadConfig();
|
|
18329
18505
|
if (options.selfHeal !== undefined)
|
|
@@ -18364,7 +18540,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
18364
18540
|
runId,
|
|
18365
18541
|
scenarioId: scenario.id,
|
|
18366
18542
|
model,
|
|
18367
|
-
stepsTotal:
|
|
18543
|
+
stepsTotal: materializedScenario.steps.length || 10,
|
|
18368
18544
|
personaId: persona?.id ?? null,
|
|
18369
18545
|
personaName: persona?.name ?? null
|
|
18370
18546
|
});
|
|
@@ -18400,12 +18576,12 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
18400
18576
|
engine: effectiveOptions.engine
|
|
18401
18577
|
});
|
|
18402
18578
|
}
|
|
18403
|
-
const targetUrl =
|
|
18404
|
-
const scenarioTimeout =
|
|
18579
|
+
const targetUrl = materializedScenario.targetPath ? resolveStartUrl(options.url.replace(/\/$/, ""), materializedScenario.targetPath) : options.url;
|
|
18580
|
+
const scenarioTimeout = materializedScenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
|
|
18405
18581
|
registerSession({
|
|
18406
18582
|
resultId: result.id,
|
|
18407
18583
|
runId,
|
|
18408
|
-
scenarioId:
|
|
18584
|
+
scenarioId: materializedScenario.id,
|
|
18409
18585
|
engine: effectiveOptions.engine ?? "playwright",
|
|
18410
18586
|
startUrl: targetUrl
|
|
18411
18587
|
});
|
|
@@ -18459,7 +18635,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
18459
18635
|
const agentResult = await withTimeout(runAgentLoop({
|
|
18460
18636
|
client,
|
|
18461
18637
|
page,
|
|
18462
|
-
scenario,
|
|
18638
|
+
scenario: materializedScenario,
|
|
18463
18639
|
screenshotter,
|
|
18464
18640
|
model,
|
|
18465
18641
|
runId,
|
|
@@ -18550,7 +18726,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
18550
18726
|
const baseReasoning = agentResult.reasoning ? agentResult.reasoning + lightpandaNote : lightpandaNote || "";
|
|
18551
18727
|
const assertionOutcome = await applyStructuredAssertionsToResult({
|
|
18552
18728
|
page,
|
|
18553
|
-
scenario,
|
|
18729
|
+
scenario: materializedScenario,
|
|
18554
18730
|
consoleErrors,
|
|
18555
18731
|
status: agentResult.status,
|
|
18556
18732
|
reasoning: baseReasoning
|
|
@@ -18571,6 +18747,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
18571
18747
|
costCents: estimateCost(model, agentResult.tokensUsed),
|
|
18572
18748
|
metadata: {
|
|
18573
18749
|
consoleLogs,
|
|
18750
|
+
...routeFixtureResolution.params.length > 0 ? { routeFixtureResolution } : {},
|
|
18574
18751
|
...networkErrors.length > 0 ? networkMeta : {},
|
|
18575
18752
|
...structuredAssertionMeta
|
|
18576
18753
|
}
|
|
@@ -18920,6 +19097,7 @@ var init_runner = __esm(() => {
|
|
|
18920
19097
|
init_browser();
|
|
18921
19098
|
init_screenshotter();
|
|
18922
19099
|
init_ai_client();
|
|
19100
|
+
init_route_fixtures();
|
|
18923
19101
|
init_config2();
|
|
18924
19102
|
init_persona_auth();
|
|
18925
19103
|
init_session_tracker();
|
|
@@ -59225,6 +59403,10 @@ function scenarioInputForNextRoute(item, projectId) {
|
|
|
59225
59403
|
actionCount: item.actions.length,
|
|
59226
59404
|
groups: item.groups
|
|
59227
59405
|
},
|
|
59406
|
+
parameters: item.fixtureParams.length > 0 ? {
|
|
59407
|
+
routeFixtures: defaultRouteFixturesForParams(item.fixtureParams),
|
|
59408
|
+
routeFixtureParams: item.fixtureParams
|
|
59409
|
+
} : undefined,
|
|
59228
59410
|
projectId
|
|
59229
59411
|
};
|
|
59230
59412
|
}
|
|
@@ -59627,6 +59809,7 @@ var ROUTE_FILE_NAMES, WALK_EXCLUDES, SAFE_PAGE_ASSERTIONS, IMPORT_SCAN_LIMIT = 4
|
|
|
59627
59809
|
var init_next_route_inventory = __esm(() => {
|
|
59628
59810
|
init_scenarios();
|
|
59629
59811
|
init_workflows();
|
|
59812
|
+
init_route_fixtures();
|
|
59630
59813
|
ROUTE_FILE_NAMES = new Set([
|
|
59631
59814
|
"page.tsx",
|
|
59632
59815
|
"page.ts",
|
|
@@ -60280,8 +60463,11 @@ var exports_workflow_fanout = {};
|
|
|
60280
60463
|
__export(exports_workflow_fanout, {
|
|
60281
60464
|
runWorkflowFanout: () => runWorkflowFanout,
|
|
60282
60465
|
resolveWorkflowFanoutSelection: () => resolveWorkflowFanoutSelection,
|
|
60283
|
-
normalizeFanoutWorkerCount: () => normalizeFanoutWorkerCount
|
|
60466
|
+
normalizeFanoutWorkerCount: () => normalizeFanoutWorkerCount,
|
|
60467
|
+
checkWorkflowFanoutReadiness: () => checkWorkflowFanoutReadiness
|
|
60284
60468
|
});
|
|
60469
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
60470
|
+
import { existsSync as existsSync19, statSync as statSync5 } from "fs";
|
|
60285
60471
|
function splitWorkflowIds(ids) {
|
|
60286
60472
|
return (ids ?? []).flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
60287
60473
|
}
|
|
@@ -60314,6 +60500,89 @@ function resolveWorkflowFanoutSelection(options) {
|
|
|
60314
60500
|
}
|
|
60315
60501
|
return filtered;
|
|
60316
60502
|
}
|
|
60503
|
+
async function checkWorkflowFanoutReadiness(workflows, dependencies = {}) {
|
|
60504
|
+
const checks = [];
|
|
60505
|
+
const env = dependencies.env ?? process.env;
|
|
60506
|
+
for (const [provider, providerWorkflows] of groupWorkflowsByProvider(workflows)) {
|
|
60507
|
+
const envKey = PROVIDER_ENV_KEYS[provider];
|
|
60508
|
+
if (!envKey) {
|
|
60509
|
+
checks.push({
|
|
60510
|
+
name: `provider:${provider}`,
|
|
60511
|
+
ok: true,
|
|
60512
|
+
required: false,
|
|
60513
|
+
message: `No built-in credential preflight for sandbox provider "${provider}"`,
|
|
60514
|
+
workflows: providerWorkflows.map((workflow) => workflow.name)
|
|
60515
|
+
});
|
|
60516
|
+
continue;
|
|
60517
|
+
}
|
|
60518
|
+
const apiKey = await resolveProviderApiKey(provider, env, dependencies.providerApiKeyResolver);
|
|
60519
|
+
checks.push({
|
|
60520
|
+
name: `provider:${provider}`,
|
|
60521
|
+
ok: Boolean(apiKey),
|
|
60522
|
+
required: true,
|
|
60523
|
+
message: apiKey ? `Sandbox provider "${provider}" credential is available` : `Missing sandbox provider credential for "${provider}". Set ${envKey} or configure providers.${provider}.api_key with sandboxes config`,
|
|
60524
|
+
workflows: providerWorkflows.map((workflow) => workflow.name),
|
|
60525
|
+
details: { provider, envKey }
|
|
60526
|
+
});
|
|
60527
|
+
}
|
|
60528
|
+
const needsRsync = workflows.filter((workflow) => (workflow.execution.sandboxSyncStrategy ?? "rsync") === "rsync" || Boolean(workflow.execution.appSourceDir));
|
|
60529
|
+
if (needsRsync.length > 0) {
|
|
60530
|
+
const commandExists = dependencies.commandExists ?? defaultCommandExists;
|
|
60531
|
+
const rsyncOk = commandExists("rsync");
|
|
60532
|
+
checks.push({
|
|
60533
|
+
name: "tool:rsync",
|
|
60534
|
+
ok: rsyncOk,
|
|
60535
|
+
required: true,
|
|
60536
|
+
message: rsyncOk ? "rsync is available for sandbox uploads and app source bundling" : "Missing rsync. Install rsync or use --sandbox-sync archive for workflows without app source bundling",
|
|
60537
|
+
workflows: needsRsync.map((workflow) => workflow.name)
|
|
60538
|
+
});
|
|
60539
|
+
}
|
|
60540
|
+
const missingAppSources = workflows.filter((workflow) => workflow.execution.appSourceDir).filter((workflow) => {
|
|
60541
|
+
const sourceDir = workflow.execution.appSourceDir;
|
|
60542
|
+
return !existsSync19(sourceDir) || !statSync5(sourceDir).isDirectory();
|
|
60543
|
+
});
|
|
60544
|
+
if (missingAppSources.length > 0) {
|
|
60545
|
+
checks.push({
|
|
60546
|
+
name: "app-source",
|
|
60547
|
+
ok: false,
|
|
60548
|
+
required: true,
|
|
60549
|
+
message: "One or more workflow app source directories are missing or are not directories",
|
|
60550
|
+
workflows: missingAppSources.map((workflow) => workflow.name),
|
|
60551
|
+
details: {
|
|
60552
|
+
sources: missingAppSources.map((workflow) => ({
|
|
60553
|
+
workflowId: workflow.id,
|
|
60554
|
+
workflowName: workflow.name,
|
|
60555
|
+
appSourceDir: workflow.execution.appSourceDir
|
|
60556
|
+
}))
|
|
60557
|
+
}
|
|
60558
|
+
});
|
|
60559
|
+
}
|
|
60560
|
+
const { requiredMissing, optionalMissing } = collectMissingSandboxEnvRefs(workflows, env, dependencies.credentialResolver);
|
|
60561
|
+
if (requiredMissing.length > 0) {
|
|
60562
|
+
checks.push({
|
|
60563
|
+
name: "env:required",
|
|
60564
|
+
ok: false,
|
|
60565
|
+
required: true,
|
|
60566
|
+
message: "One or more required sandbox environment references could not be resolved",
|
|
60567
|
+
workflows: [...new Set(requiredMissing.map((item) => item.workflowName))],
|
|
60568
|
+
details: { missing: requiredMissing }
|
|
60569
|
+
});
|
|
60570
|
+
}
|
|
60571
|
+
if (optionalMissing.length > 0) {
|
|
60572
|
+
checks.push({
|
|
60573
|
+
name: "env:optional",
|
|
60574
|
+
ok: false,
|
|
60575
|
+
required: false,
|
|
60576
|
+
message: "One or more optional sandbox environment references are not set and will be omitted",
|
|
60577
|
+
workflows: [...new Set(optionalMissing.map((item) => item.workflowName))],
|
|
60578
|
+
details: { missing: optionalMissing }
|
|
60579
|
+
});
|
|
60580
|
+
}
|
|
60581
|
+
return {
|
|
60582
|
+
ok: checks.every((check) => check.ok || !check.required),
|
|
60583
|
+
checks
|
|
60584
|
+
};
|
|
60585
|
+
}
|
|
60317
60586
|
async function mapWithConcurrency(items, limit, worker) {
|
|
60318
60587
|
const output = new Array(items.length);
|
|
60319
60588
|
let next = 0;
|
|
@@ -60329,7 +60598,38 @@ async function mapWithConcurrency(items, limit, worker) {
|
|
|
60329
60598
|
async function runWorkflowFanout(options, dependencies = {}) {
|
|
60330
60599
|
const workers = normalizeFanoutWorkerCount(options.workers);
|
|
60331
60600
|
const workflows = resolveWorkflowFanoutSelection(options);
|
|
60332
|
-
const {
|
|
60601
|
+
const {
|
|
60602
|
+
runTestingWorkflow: runOne = runTestingWorkflow,
|
|
60603
|
+
preflight: preflightOverride,
|
|
60604
|
+
providerApiKeyResolver,
|
|
60605
|
+
commandExists,
|
|
60606
|
+
credentialResolver,
|
|
60607
|
+
env,
|
|
60608
|
+
...workflowDependencies
|
|
60609
|
+
} = dependencies;
|
|
60610
|
+
const preflight = preflightOverride ? await preflightOverride(workflows) : await checkWorkflowFanoutReadiness(workflows, {
|
|
60611
|
+
providerApiKeyResolver,
|
|
60612
|
+
commandExists,
|
|
60613
|
+
credentialResolver,
|
|
60614
|
+
env
|
|
60615
|
+
});
|
|
60616
|
+
if (!options.dryRun && !preflight.ok) {
|
|
60617
|
+
const error = `Preflight failed: ${summarizePreflightFailures(preflight)}`;
|
|
60618
|
+
return {
|
|
60619
|
+
status: "failed",
|
|
60620
|
+
workers,
|
|
60621
|
+
total: workflows.length,
|
|
60622
|
+
passed: 0,
|
|
60623
|
+
failed: workflows.length,
|
|
60624
|
+
preflight,
|
|
60625
|
+
items: workflows.map((workflow) => ({
|
|
60626
|
+
workflowId: workflow.id,
|
|
60627
|
+
workflowName: workflow.name,
|
|
60628
|
+
status: "failed",
|
|
60629
|
+
error
|
|
60630
|
+
}))
|
|
60631
|
+
};
|
|
60632
|
+
}
|
|
60333
60633
|
const items = await mapWithConcurrency(workflows, workers, async (workflow) => {
|
|
60334
60634
|
try {
|
|
60335
60635
|
const output = await runOne(workflow.id, {
|
|
@@ -60376,12 +60676,81 @@ async function runWorkflowFanout(options, dependencies = {}) {
|
|
|
60376
60676
|
total: items.length,
|
|
60377
60677
|
passed,
|
|
60378
60678
|
failed,
|
|
60379
|
-
items
|
|
60679
|
+
items,
|
|
60680
|
+
preflight
|
|
60380
60681
|
};
|
|
60381
60682
|
}
|
|
60683
|
+
function groupWorkflowsByProvider(workflows) {
|
|
60684
|
+
const byProvider = new Map;
|
|
60685
|
+
for (const workflow of workflows) {
|
|
60686
|
+
const provider = workflow.execution.provider ?? "e2b";
|
|
60687
|
+
byProvider.set(provider, [...byProvider.get(provider) ?? [], workflow]);
|
|
60688
|
+
}
|
|
60689
|
+
return byProvider;
|
|
60690
|
+
}
|
|
60691
|
+
async function resolveProviderApiKey(provider, env, resolver) {
|
|
60692
|
+
if (resolver)
|
|
60693
|
+
return resolver(provider, env);
|
|
60694
|
+
const envKey = PROVIDER_ENV_KEYS[provider];
|
|
60695
|
+
if (envKey && env[envKey])
|
|
60696
|
+
return env[envKey];
|
|
60697
|
+
try {
|
|
60698
|
+
const mod = await import("@hasna/sandboxes");
|
|
60699
|
+
if (provider === "e2b" || provider === "daytona" || provider === "modal") {
|
|
60700
|
+
return mod.getProviderApiKey?.(provider);
|
|
60701
|
+
}
|
|
60702
|
+
} catch {}
|
|
60703
|
+
return;
|
|
60704
|
+
}
|
|
60705
|
+
function defaultCommandExists(command) {
|
|
60706
|
+
const result = spawnSync2(command, ["--version"], { encoding: "utf8" });
|
|
60707
|
+
return !result.error && result.status === 0;
|
|
60708
|
+
}
|
|
60709
|
+
function collectMissingSandboxEnvRefs(workflows, env, credentialResolver) {
|
|
60710
|
+
const requiredMissing = [];
|
|
60711
|
+
const optionalMissing = [];
|
|
60712
|
+
for (const workflow of workflows) {
|
|
60713
|
+
for (const [key, value] of Object.entries(workflow.execution.env ?? {})) {
|
|
60714
|
+
if (value.startsWith("$?")) {
|
|
60715
|
+
const name = value.slice(2).trim();
|
|
60716
|
+
if (name && env[name] === undefined) {
|
|
60717
|
+
optionalMissing.push({ workflowId: workflow.id, workflowName: workflow.name, key, reference: value });
|
|
60718
|
+
}
|
|
60719
|
+
continue;
|
|
60720
|
+
}
|
|
60721
|
+
if (!isResolvableEnvReference(value))
|
|
60722
|
+
continue;
|
|
60723
|
+
if (resolveSandboxEnvReference(value, env, credentialResolver) === null) {
|
|
60724
|
+
requiredMissing.push({ workflowId: workflow.id, workflowName: workflow.name, key, reference: value });
|
|
60725
|
+
}
|
|
60726
|
+
}
|
|
60727
|
+
}
|
|
60728
|
+
return { requiredMissing, optionalMissing };
|
|
60729
|
+
}
|
|
60730
|
+
function isResolvableEnvReference(value) {
|
|
60731
|
+
return value.startsWith("$") || value.startsWith("@secrets:");
|
|
60732
|
+
}
|
|
60733
|
+
function resolveSandboxEnvReference(value, env, credentialResolver) {
|
|
60734
|
+
if (value.startsWith("$")) {
|
|
60735
|
+
const varName = value.slice(1).trim();
|
|
60736
|
+
return varName ? env[varName] ?? null : null;
|
|
60737
|
+
}
|
|
60738
|
+
return (credentialResolver ?? resolveCredential)(value);
|
|
60739
|
+
}
|
|
60740
|
+
function summarizePreflightFailures(preflight) {
|
|
60741
|
+
const requiredFailures = preflight.checks.filter((check) => !check.ok && check.required);
|
|
60742
|
+
return requiredFailures.length > 0 ? requiredFailures.map((check) => check.message).join("; ") : "required checks did not pass";
|
|
60743
|
+
}
|
|
60744
|
+
var PROVIDER_ENV_KEYS;
|
|
60382
60745
|
var init_workflow_fanout = __esm(() => {
|
|
60383
60746
|
init_workflows();
|
|
60384
60747
|
init_workflow_runner();
|
|
60748
|
+
init_secrets_resolver();
|
|
60749
|
+
PROVIDER_ENV_KEYS = {
|
|
60750
|
+
e2b: "E2B_API_KEY",
|
|
60751
|
+
daytona: "DAYTONA_API_KEY",
|
|
60752
|
+
modal: "MODAL_TOKEN_ID"
|
|
60753
|
+
};
|
|
60385
60754
|
});
|
|
60386
60755
|
|
|
60387
60756
|
// node_modules/@ai-sdk/provider/dist/index.mjs
|
|
@@ -95041,7 +95410,7 @@ import chalk6 from "chalk";
|
|
|
95041
95410
|
// package.json
|
|
95042
95411
|
var package_default = {
|
|
95043
95412
|
name: "@hasna/testers",
|
|
95044
|
-
version: "0.0.
|
|
95413
|
+
version: "0.0.52",
|
|
95045
95414
|
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
95046
95415
|
type: "module",
|
|
95047
95416
|
main: "dist/index.js",
|
|
@@ -97228,7 +97597,7 @@ init_ci();
|
|
|
97228
97597
|
init_assertions();
|
|
97229
97598
|
init_paths();
|
|
97230
97599
|
init_sessions();
|
|
97231
|
-
import { existsSync as
|
|
97600
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync15 } from "fs";
|
|
97232
97601
|
|
|
97233
97602
|
// src/lib/repo-discovery.ts
|
|
97234
97603
|
init_paths();
|
|
@@ -98257,7 +98626,7 @@ var CONFIG_DIR5 = getTestersDir();
|
|
|
98257
98626
|
var CONFIG_PATH4 = join21(CONFIG_DIR5, "config.json");
|
|
98258
98627
|
function getActiveProject() {
|
|
98259
98628
|
try {
|
|
98260
|
-
if (
|
|
98629
|
+
if (existsSync20(CONFIG_PATH4)) {
|
|
98261
98630
|
const raw = JSON.parse(readFileSync11(CONFIG_PATH4, "utf-8"));
|
|
98262
98631
|
return raw.activeProject ?? undefined;
|
|
98263
98632
|
}
|
|
@@ -99188,7 +99557,7 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
|
|
|
99188
99557
|
return;
|
|
99189
99558
|
}
|
|
99190
99559
|
const outputDir = opts.output ?? ".";
|
|
99191
|
-
if (!
|
|
99560
|
+
if (!existsSync20(outputDir)) {
|
|
99192
99561
|
mkdirSync15(outputDir, { recursive: true });
|
|
99193
99562
|
}
|
|
99194
99563
|
for (const s2 of scenarios) {
|
|
@@ -99391,11 +99760,11 @@ projectCmd.command("export-open <id>").description("Register a testers project i
|
|
|
99391
99760
|
projectCmd.command("use <name>").description("Set active project (find or create)").option("--json", "Output as JSON", false).action((name21, opts) => {
|
|
99392
99761
|
try {
|
|
99393
99762
|
const project = ensureProject(name21, process.cwd());
|
|
99394
|
-
if (!
|
|
99763
|
+
if (!existsSync20(CONFIG_DIR5)) {
|
|
99395
99764
|
mkdirSync15(CONFIG_DIR5, { recursive: true });
|
|
99396
99765
|
}
|
|
99397
99766
|
let config2 = {};
|
|
99398
|
-
if (
|
|
99767
|
+
if (existsSync20(CONFIG_PATH4)) {
|
|
99399
99768
|
try {
|
|
99400
99769
|
config2 = JSON.parse(readFileSync11(CONFIG_PATH4, "utf-8"));
|
|
99401
99770
|
} catch {}
|
|
@@ -100010,7 +100379,7 @@ program2.command("ci [provider]").description("Print or write a CI workflow (def
|
|
|
100010
100379
|
if (opts.output) {
|
|
100011
100380
|
const outPath = resolve5(opts.output);
|
|
100012
100381
|
const outDir = outPath.replace(/\/[^/]*$/, "");
|
|
100013
|
-
if (outDir && !
|
|
100382
|
+
if (outDir && !existsSync20(outDir)) {
|
|
100014
100383
|
mkdirSync15(outDir, { recursive: true });
|
|
100015
100384
|
}
|
|
100016
100385
|
writeFileSync7(outPath, workflow, "utf-8");
|
|
@@ -100045,7 +100414,7 @@ program2.command("init").description("Initialize a new testing project").option(
|
|
|
100045
100414
|
}
|
|
100046
100415
|
if (opts.ci === "github") {
|
|
100047
100416
|
const workflowDir = join21(process.cwd(), ".github", "workflows");
|
|
100048
|
-
if (!
|
|
100417
|
+
if (!existsSync20(workflowDir)) {
|
|
100049
100418
|
mkdirSync15(workflowDir, { recursive: true });
|
|
100050
100419
|
}
|
|
100051
100420
|
const workflowPath = join21(workflowDir, "testers.yml");
|
|
@@ -101821,6 +102190,19 @@ workflowCmd.command("fanout [ids...]").description("Run multiple saved sandbox w
|
|
|
101821
102190
|
if (opts.json || opts.dryRun) {
|
|
101822
102191
|
log(JSON.stringify(result, null, 2));
|
|
101823
102192
|
} else {
|
|
102193
|
+
const preflightChecks = result.preflight?.checks ?? [];
|
|
102194
|
+
const failedRequiredChecks = preflightChecks.filter((check2) => !check2.ok && check2.required);
|
|
102195
|
+
const warnings = preflightChecks.filter((check2) => !check2.ok && !check2.required);
|
|
102196
|
+
if (failedRequiredChecks.length > 0 || warnings.length > 0) {
|
|
102197
|
+
const label = failedRequiredChecks.length > 0 ? chalk6.red("failed") : chalk6.yellow("warnings");
|
|
102198
|
+
log(chalk6.bold(`Preflight ${label}:`));
|
|
102199
|
+
for (const check2 of failedRequiredChecks) {
|
|
102200
|
+
log(` ${chalk6.red("failed")} ${check2.message}`);
|
|
102201
|
+
}
|
|
102202
|
+
for (const check2 of warnings) {
|
|
102203
|
+
log(` ${chalk6.yellow("warning")} ${check2.message}`);
|
|
102204
|
+
}
|
|
102205
|
+
}
|
|
101824
102206
|
const status = result.status === "passed" ? chalk6.green("passed") : chalk6.red("failed");
|
|
101825
102207
|
log(chalk6.bold(`Sandbox workflow fanout ${status}: ${result.passed}/${result.total} passed with ${result.workers} worker(s)`));
|
|
101826
102208
|
for (const item of result.items) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scenarios.d.ts","sourceRoot":"","sources":["../../src/db/scenarios.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EAEb,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAGpB,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"scenarios.d.ts","sourceRoot":"","sources":["../../src/db/scenarios.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EAEb,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAGpB,MAAM,mBAAmB,CAAC;AA2C3B,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,QAAQ,CA+BnE;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAmBvD;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAIrE;AAED,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,QAAQ,EAAE,CAoFjE;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ,CAwFhG;AAED,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,MAAM,CA8B9D;AAED,MAAM,WAAW,aAAc,SAAQ,QAAQ;IAC7C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,EAAE,CAmBhE;AAED,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAIvE;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAOlD;AAED,MAAM,MAAM,oBAAoB,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAA;CAAE,CAAC;AAErG;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,oBAAoB,CAgElG"}
|