@hasna/testers 0.0.49 → 0.0.51
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 +428 -31
- 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 +11 -0
- 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/mcp/index.js +181 -25
- package/dist/server/index.js +181 -25
- package/package.json +1 -1
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();
|
|
@@ -59184,17 +59362,22 @@ function discoverNextRouteInventory(options) {
|
|
|
59184
59362
|
function scenarioInputForNextRoute(item, projectId) {
|
|
59185
59363
|
const label = item.kind === "page" ? "page" : "API route";
|
|
59186
59364
|
const methodList = item.methods.length > 0 ? item.methods.join(", ") : "discovered methods";
|
|
59365
|
+
const fixtureStep = item.fixtureParams.length > 0 ? `Bind dynamic fixture values for ${item.fixtureParams.map((name) => `:${name}`).join(", ")} before running route actions.` : undefined;
|
|
59187
59366
|
const dynamicStep = item.dynamic ? "Substitute dynamic path parameters with valid fixture values from the target org before opening or calling the route." : undefined;
|
|
59367
|
+
const actionSteps = item.actions.slice(0, 16).map(formatActionStep);
|
|
59188
59368
|
const pageSteps = [
|
|
59369
|
+
fixtureStep,
|
|
59189
59370
|
dynamicStep,
|
|
59190
59371
|
`Open the Next.js ${label} ${item.routePath}.`,
|
|
59191
59372
|
"Wait for the route to finish loading and verify it does not show a blank shell, framework error page, or unexpected auth loop.",
|
|
59192
|
-
"Exercise visible primary navigation, tabs, filters, dialogs, forms, and safe buttons on this route.",
|
|
59373
|
+
...actionSteps.length > 0 ? actionSteps : ["Exercise visible primary navigation, tabs, filters, dialogs, forms, and safe buttons on this route."],
|
|
59193
59374
|
"Verify the route stays within the expected org/workspace context and does not emit console errors."
|
|
59194
59375
|
].filter(Boolean);
|
|
59195
59376
|
const apiSteps = [
|
|
59377
|
+
fixtureStep,
|
|
59196
59378
|
dynamicStep,
|
|
59197
59379
|
`Call the ${methodList} handler(s) for ${item.routePath} using safe fixture data.`,
|
|
59380
|
+
...actionSteps,
|
|
59198
59381
|
"Verify expected authentication, authorization, validation, and tenant isolation behavior.",
|
|
59199
59382
|
"For mutating methods, use harmless test payloads and confirm the response does not create cross-org side effects.",
|
|
59200
59383
|
"Verify response status, JSON shape, and error messages are stable and regression-safe."
|
|
@@ -59215,8 +59398,15 @@ function scenarioInputForNextRoute(item, projectId) {
|
|
|
59215
59398
|
category: item.category,
|
|
59216
59399
|
methods: item.methods,
|
|
59217
59400
|
dynamic: item.dynamic,
|
|
59401
|
+
fixtureParams: item.fixtureParams,
|
|
59402
|
+
actions: item.actions,
|
|
59403
|
+
actionCount: item.actions.length,
|
|
59218
59404
|
groups: item.groups
|
|
59219
59405
|
},
|
|
59406
|
+
parameters: item.fixtureParams.length > 0 ? {
|
|
59407
|
+
routeFixtures: defaultRouteFixturesForParams(item.fixtureParams),
|
|
59408
|
+
routeFixtureParams: item.fixtureParams
|
|
59409
|
+
} : undefined,
|
|
59220
59410
|
projectId
|
|
59221
59411
|
};
|
|
59222
59412
|
}
|
|
@@ -59312,9 +59502,20 @@ function routeItemFromFile(rootDir, appDir, file) {
|
|
|
59312
59502
|
const pathSegments = routeSegments.filter((segment) => !segment.startsWith("(")).filter((segment) => !segment.startsWith("@")).map(normalizeRouteSegment).filter(Boolean);
|
|
59313
59503
|
const routePath = `/${pathSegments.join("/")}`.replace(/\/+/g, "/");
|
|
59314
59504
|
const normalizedRoutePath = routePath === "/" ? "/" : routePath.replace(/\/$/, "");
|
|
59315
|
-
const
|
|
59505
|
+
const sources = collectRouteSources(rootDir, file);
|
|
59506
|
+
const primarySource = sources[0]?.source ?? readFileSync9(file, "utf8");
|
|
59507
|
+
const methods = kind === "api" ? extractRouteMethods(primarySource) : [];
|
|
59316
59508
|
const category = classifyRoute(normalizedRoutePath, groups, relativeFile);
|
|
59317
59509
|
const dynamic = routeSegments.some((segment) => segment.includes("["));
|
|
59510
|
+
const fixtureParams = extractFixtureParams(normalizedRoutePath);
|
|
59511
|
+
const actions = kind === "api" ? methods.map((method) => ({
|
|
59512
|
+
kind: "api-method",
|
|
59513
|
+
label: method,
|
|
59514
|
+
target: normalizedRoutePath,
|
|
59515
|
+
sourceFile: relativeFile,
|
|
59516
|
+
destructive: isDestructiveAction(method, normalizedRoutePath),
|
|
59517
|
+
requiresFixture: fixtureParams.length > 0
|
|
59518
|
+
})) : extractPageActions(rootDir, sources, fixtureParams);
|
|
59318
59519
|
const requiresAuth = inferRequiresAuth(normalizedRoutePath, groups, kind);
|
|
59319
59520
|
return {
|
|
59320
59521
|
kind,
|
|
@@ -59325,6 +59526,8 @@ function routeItemFromFile(rootDir, appDir, file) {
|
|
|
59325
59526
|
methods,
|
|
59326
59527
|
dynamic,
|
|
59327
59528
|
requiresAuth,
|
|
59529
|
+
fixtureParams,
|
|
59530
|
+
actions,
|
|
59328
59531
|
tags: tagsForRoute({ kind, routePath: normalizedRoutePath, category, groups, dynamic, requiresAuth }),
|
|
59329
59532
|
priority: priorityForRoute(normalizedRoutePath, category, kind)
|
|
59330
59533
|
};
|
|
@@ -59341,8 +59544,7 @@ function normalizeRouteSegment(segment) {
|
|
|
59341
59544
|
}
|
|
59342
59545
|
return segment;
|
|
59343
59546
|
}
|
|
59344
|
-
function extractRouteMethods(
|
|
59345
|
-
const source = readFileSync9(file, "utf8");
|
|
59547
|
+
function extractRouteMethods(source) {
|
|
59346
59548
|
const methods = new Set;
|
|
59347
59549
|
const pattern = /\b(?:export\s+)?(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b|\bexport\s+const\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/g;
|
|
59348
59550
|
for (const match of source.matchAll(pattern)) {
|
|
@@ -59352,6 +59554,193 @@ function extractRouteMethods(file) {
|
|
|
59352
59554
|
}
|
|
59353
59555
|
return [...methods].sort();
|
|
59354
59556
|
}
|
|
59557
|
+
function collectRouteSources(rootDir, entryFile) {
|
|
59558
|
+
const seen = new Set;
|
|
59559
|
+
const sources = [];
|
|
59560
|
+
function visit(file, depth) {
|
|
59561
|
+
if (seen.has(file) || sources.length >= IMPORT_SCAN_LIMIT)
|
|
59562
|
+
return;
|
|
59563
|
+
if (!existsSync18(file) || !statSync4(file).isFile())
|
|
59564
|
+
return;
|
|
59565
|
+
seen.add(file);
|
|
59566
|
+
const source = readFileSync9(file, "utf8");
|
|
59567
|
+
sources.push({ file: relative4(rootDir, file), source });
|
|
59568
|
+
if (depth >= IMPORT_SCAN_DEPTH)
|
|
59569
|
+
return;
|
|
59570
|
+
for (const specifier of localImportSpecifiers(source)) {
|
|
59571
|
+
const resolved = resolveImportFile(file, specifier);
|
|
59572
|
+
if (resolved)
|
|
59573
|
+
visit(resolved, depth + 1);
|
|
59574
|
+
}
|
|
59575
|
+
}
|
|
59576
|
+
visit(entryFile, 0);
|
|
59577
|
+
return sources;
|
|
59578
|
+
}
|
|
59579
|
+
function localImportSpecifiers(source) {
|
|
59580
|
+
const specifiers = [];
|
|
59581
|
+
const pattern = /\bimport\b(?:[\s\S]*?\bfrom\s*)?["'](\.{1,2}\/[^"']+)["']|\bimport\(\s*["'](\.{1,2}\/[^"']+)["']\s*\)/g;
|
|
59582
|
+
for (const match of source.matchAll(pattern)) {
|
|
59583
|
+
const specifier = match[1] ?? match[2];
|
|
59584
|
+
if (specifier)
|
|
59585
|
+
specifiers.push(specifier);
|
|
59586
|
+
}
|
|
59587
|
+
return specifiers;
|
|
59588
|
+
}
|
|
59589
|
+
function resolveImportFile(fromFile, specifier) {
|
|
59590
|
+
const base = resolve3(join20(fromFile, ".."), specifier);
|
|
59591
|
+
const candidates = [
|
|
59592
|
+
base,
|
|
59593
|
+
...SOURCE_EXTENSIONS.map((ext) => `${base}${ext}`),
|
|
59594
|
+
...SOURCE_EXTENSIONS.map((ext) => join20(base, `index${ext}`))
|
|
59595
|
+
];
|
|
59596
|
+
return candidates.find((candidate) => existsSync18(candidate) && statSync4(candidate).isFile()) ?? null;
|
|
59597
|
+
}
|
|
59598
|
+
function extractPageActions(rootDir, sources, fixtureParams) {
|
|
59599
|
+
const actions = [];
|
|
59600
|
+
for (const source of sources) {
|
|
59601
|
+
actions.push(...extractLinkedActions(source, fixtureParams));
|
|
59602
|
+
actions.push(...extractButtonActions(source, fixtureParams));
|
|
59603
|
+
actions.push(...extractFormActions(source, fixtureParams));
|
|
59604
|
+
actions.push(...extractInputActions(source, fixtureParams));
|
|
59605
|
+
}
|
|
59606
|
+
const deduped = new Map;
|
|
59607
|
+
for (const action of actions) {
|
|
59608
|
+
const key = [
|
|
59609
|
+
action.kind,
|
|
59610
|
+
normalizeLabel(action.label),
|
|
59611
|
+
action.target ?? "",
|
|
59612
|
+
action.sourceFile
|
|
59613
|
+
].join("|");
|
|
59614
|
+
if (!deduped.has(key))
|
|
59615
|
+
deduped.set(key, action);
|
|
59616
|
+
}
|
|
59617
|
+
return [...deduped.values()].sort((a2, b2) => `${a2.kind}:${a2.label}:${a2.target ?? ""}`.localeCompare(`${b2.kind}:${b2.label}:${b2.target ?? ""}`)).slice(0, 40).map((action) => ({ ...action, sourceFile: relative4(rootDir, resolve3(rootDir, action.sourceFile)) }));
|
|
59618
|
+
}
|
|
59619
|
+
function extractLinkedActions(source, fixtureParams) {
|
|
59620
|
+
const actions = [];
|
|
59621
|
+
const pattern = /<(Link|a)\b([^>]*)>([\s\S]*?)<\/\1>/g;
|
|
59622
|
+
for (const match of source.source.matchAll(pattern)) {
|
|
59623
|
+
const attrs = match[2] ?? "";
|
|
59624
|
+
const body = match[3] ?? "";
|
|
59625
|
+
const href = attributeValue(attrs, "href");
|
|
59626
|
+
const label = firstNonEmpty(attributeValue(attrs, "aria-label"), attributeValue(attrs, "title"), textFromJsx(body), href);
|
|
59627
|
+
if (!label)
|
|
59628
|
+
continue;
|
|
59629
|
+
actions.push({
|
|
59630
|
+
kind: "link",
|
|
59631
|
+
label: clamp(label),
|
|
59632
|
+
target: href ? clamp(href, 180) : undefined,
|
|
59633
|
+
sourceFile: source.file,
|
|
59634
|
+
destructive: isDestructiveAction(label, href ?? ""),
|
|
59635
|
+
requiresFixture: requiresFixture(href ?? "", fixtureParams)
|
|
59636
|
+
});
|
|
59637
|
+
}
|
|
59638
|
+
return actions;
|
|
59639
|
+
}
|
|
59640
|
+
function extractButtonActions(source, fixtureParams) {
|
|
59641
|
+
const actions = [];
|
|
59642
|
+
const pattern = /<(button|Button|IconButton|DropdownMenuItem|CommandItem|SelectItem|TabsTrigger)\b([^>]*?)(?:>([\s\S]*?)<\/\1>|\/>)/g;
|
|
59643
|
+
for (const match of source.source.matchAll(pattern)) {
|
|
59644
|
+
const attrs = match[2] ?? "";
|
|
59645
|
+
const body = match[3] ?? "";
|
|
59646
|
+
const label = firstNonEmpty(attributeValue(attrs, "aria-label"), attributeValue(attrs, "title"), attributeValue(attrs, "data-testid"), textFromJsx(body), attributeValue(attrs, "value"));
|
|
59647
|
+
if (!label)
|
|
59648
|
+
continue;
|
|
59649
|
+
const target = attributeValue(attrs, "href") ?? attributeValue(attrs, "data-testid");
|
|
59650
|
+
actions.push({
|
|
59651
|
+
kind: "button",
|
|
59652
|
+
label: clamp(label),
|
|
59653
|
+
target: target ? clamp(target, 180) : undefined,
|
|
59654
|
+
sourceFile: source.file,
|
|
59655
|
+
destructive: isDestructiveAction(label, attrs),
|
|
59656
|
+
requiresFixture: requiresFixture(`${label} ${target ?? ""}`, fixtureParams)
|
|
59657
|
+
});
|
|
59658
|
+
}
|
|
59659
|
+
return actions;
|
|
59660
|
+
}
|
|
59661
|
+
function extractFormActions(source, fixtureParams) {
|
|
59662
|
+
const actions = [];
|
|
59663
|
+
const pattern = /<form\b([^>]*)>/g;
|
|
59664
|
+
for (const match of source.source.matchAll(pattern)) {
|
|
59665
|
+
const attrs = match[1] ?? "";
|
|
59666
|
+
const label = firstNonEmpty(attributeValue(attrs, "aria-label"), attributeValue(attrs, "name"), attributeValue(attrs, "data-testid"), attributeValue(attrs, "action"), `form in ${source.file}`);
|
|
59667
|
+
const target = attributeValue(attrs, "action");
|
|
59668
|
+
actions.push({
|
|
59669
|
+
kind: "form",
|
|
59670
|
+
label: clamp(label),
|
|
59671
|
+
target: target ? clamp(target, 180) : undefined,
|
|
59672
|
+
sourceFile: source.file,
|
|
59673
|
+
destructive: isDestructiveAction(label, attrs),
|
|
59674
|
+
requiresFixture: requiresFixture(`${label} ${target ?? ""}`, fixtureParams)
|
|
59675
|
+
});
|
|
59676
|
+
}
|
|
59677
|
+
return actions;
|
|
59678
|
+
}
|
|
59679
|
+
function extractInputActions(source, fixtureParams) {
|
|
59680
|
+
const actions = [];
|
|
59681
|
+
const pattern = /<(input|Input|Textarea|Select|Combobox)\b([^>]*?)(?:\/>|>)/g;
|
|
59682
|
+
for (const match of source.source.matchAll(pattern)) {
|
|
59683
|
+
const attrs = match[2] ?? "";
|
|
59684
|
+
const label = firstNonEmpty(attributeValue(attrs, "aria-label"), attributeValue(attrs, "placeholder"), attributeValue(attrs, "name"), attributeValue(attrs, "data-testid"));
|
|
59685
|
+
if (!label)
|
|
59686
|
+
continue;
|
|
59687
|
+
actions.push({
|
|
59688
|
+
kind: "input",
|
|
59689
|
+
label: clamp(label),
|
|
59690
|
+
target: attributeValue(attrs, "name") ?? attributeValue(attrs, "data-testid") ?? undefined,
|
|
59691
|
+
sourceFile: source.file,
|
|
59692
|
+
destructive: false,
|
|
59693
|
+
requiresFixture: requiresFixture(label, fixtureParams)
|
|
59694
|
+
});
|
|
59695
|
+
}
|
|
59696
|
+
return actions;
|
|
59697
|
+
}
|
|
59698
|
+
function attributeValue(attrs, name) {
|
|
59699
|
+
const pattern = new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|\\{\\s*["']([^"']+)["']\\s*\\})`, "i");
|
|
59700
|
+
const match = attrs.match(pattern);
|
|
59701
|
+
const value = match?.[1] ?? match?.[2] ?? match?.[3];
|
|
59702
|
+
return value?.trim() || undefined;
|
|
59703
|
+
}
|
|
59704
|
+
function textFromJsx(value) {
|
|
59705
|
+
const text = value.replace(/<[^>]+>/g, " ").replace(/\{[^}]*\}/g, " ").replace(/\s+/g, " ").trim();
|
|
59706
|
+
return text || undefined;
|
|
59707
|
+
}
|
|
59708
|
+
function firstNonEmpty(...values) {
|
|
59709
|
+
return values.find((value) => value && value.trim())?.trim() ?? "";
|
|
59710
|
+
}
|
|
59711
|
+
function clamp(value, max = 120) {
|
|
59712
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
59713
|
+
return compact.length > max ? `${compact.slice(0, max - 1)}\u2026` : compact;
|
|
59714
|
+
}
|
|
59715
|
+
function normalizeLabel(value) {
|
|
59716
|
+
return value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
59717
|
+
}
|
|
59718
|
+
function extractFixtureParams(routePath) {
|
|
59719
|
+
const params = new Set;
|
|
59720
|
+
for (const match of routePath.matchAll(/:([A-Za-z0-9_]+)(?:\*\??)?/g)) {
|
|
59721
|
+
if (match[1])
|
|
59722
|
+
params.add(match[1]);
|
|
59723
|
+
}
|
|
59724
|
+
return [...params];
|
|
59725
|
+
}
|
|
59726
|
+
function requiresFixture(value, fixtureParams) {
|
|
59727
|
+
if (fixtureParams.length === 0)
|
|
59728
|
+
return false;
|
|
59729
|
+
const normalized = value.toLowerCase();
|
|
59730
|
+
return fixtureParams.some((param) => normalized.includes(param.toLowerCase()) || normalized.includes(`[${param}]`));
|
|
59731
|
+
}
|
|
59732
|
+
function isDestructiveAction(label, context = "") {
|
|
59733
|
+
return /\b(delete|destroy|remove|revoke|refund|void|archive|suspend|pause|disable|cancel|reset|purge|terminate)\b/i.test(`${label} ${context}`);
|
|
59734
|
+
}
|
|
59735
|
+
function formatActionStep(action) {
|
|
59736
|
+
const target = action.target ? ` (${action.target})` : "";
|
|
59737
|
+
const fixture = action.requiresFixture ? " after binding fixture values" : "";
|
|
59738
|
+
const guard = action.destructive ? " without confirming the destructive final action" : "";
|
|
59739
|
+
if (action.kind === "api-method") {
|
|
59740
|
+
return `Exercise API method ${action.label}${target}${fixture}${guard}.`;
|
|
59741
|
+
}
|
|
59742
|
+
return `Exercise ${action.kind} "${action.label}"${target}${fixture}${guard}.`;
|
|
59743
|
+
}
|
|
59355
59744
|
function classifyRoute(routePath, groups, file) {
|
|
59356
59745
|
const haystack = `${routePath} ${groups.join(" ")} ${file}`.toLowerCase();
|
|
59357
59746
|
if (haystack.includes("admin"))
|
|
@@ -59416,10 +59805,11 @@ function priorityForRoute(routePath, category, kind) {
|
|
|
59416
59805
|
return "medium";
|
|
59417
59806
|
return "medium";
|
|
59418
59807
|
}
|
|
59419
|
-
var ROUTE_FILE_NAMES, WALK_EXCLUDES, SAFE_PAGE_ASSERTIONS;
|
|
59808
|
+
var ROUTE_FILE_NAMES, WALK_EXCLUDES, SAFE_PAGE_ASSERTIONS, IMPORT_SCAN_LIMIT = 40, IMPORT_SCAN_DEPTH = 3, SOURCE_EXTENSIONS;
|
|
59420
59809
|
var init_next_route_inventory = __esm(() => {
|
|
59421
59810
|
init_scenarios();
|
|
59422
59811
|
init_workflows();
|
|
59812
|
+
init_route_fixtures();
|
|
59423
59813
|
ROUTE_FILE_NAMES = new Set([
|
|
59424
59814
|
"page.tsx",
|
|
59425
59815
|
"page.ts",
|
|
@@ -59439,6 +59829,13 @@ var init_next_route_inventory = __esm(() => {
|
|
|
59439
59829
|
"coverage"
|
|
59440
59830
|
]);
|
|
59441
59831
|
SAFE_PAGE_ASSERTIONS = [{ type: "no_console_errors" }];
|
|
59832
|
+
SOURCE_EXTENSIONS = [
|
|
59833
|
+
".tsx",
|
|
59834
|
+
".ts",
|
|
59835
|
+
".jsx",
|
|
59836
|
+
".js",
|
|
59837
|
+
".mdx"
|
|
59838
|
+
];
|
|
59442
59839
|
});
|
|
59443
59840
|
|
|
59444
59841
|
// src/lib/generator.ts
|
|
@@ -94827,7 +95224,7 @@ import chalk6 from "chalk";
|
|
|
94827
95224
|
// package.json
|
|
94828
95225
|
var package_default = {
|
|
94829
95226
|
name: "@hasna/testers",
|
|
94830
|
-
version: "0.0.
|
|
95227
|
+
version: "0.0.51",
|
|
94831
95228
|
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
94832
95229
|
type: "module",
|
|
94833
95230
|
main: "dist/index.js",
|
|
@@ -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"}
|