@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/dist/mcp/index.js
CHANGED
|
@@ -52,7 +52,7 @@ var package_default;
|
|
|
52
52
|
var init_package = __esm(() => {
|
|
53
53
|
package_default = {
|
|
54
54
|
name: "@hasna/testers",
|
|
55
|
-
version: "0.0.
|
|
55
|
+
version: "0.0.52",
|
|
56
56
|
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
57
57
|
type: "module",
|
|
58
58
|
main: "dist/index.js",
|
|
@@ -15674,6 +15674,156 @@ var init_agents = __esm(() => {
|
|
|
15674
15674
|
init_database();
|
|
15675
15675
|
});
|
|
15676
15676
|
|
|
15677
|
+
// src/lib/route-fixtures.ts
|
|
15678
|
+
function isRecord2(value) {
|
|
15679
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15680
|
+
}
|
|
15681
|
+
function envNameForParam(prefix, param) {
|
|
15682
|
+
return `${prefix}_${param.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}`;
|
|
15683
|
+
}
|
|
15684
|
+
function readString(value) {
|
|
15685
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
15686
|
+
}
|
|
15687
|
+
function resolveReference(value, env) {
|
|
15688
|
+
if (value.startsWith("$?"))
|
|
15689
|
+
return env[value.slice(2)]?.trim() || undefined;
|
|
15690
|
+
if (value.startsWith("$"))
|
|
15691
|
+
return env[value.slice(1)]?.trim() || undefined;
|
|
15692
|
+
return value;
|
|
15693
|
+
}
|
|
15694
|
+
function scenarioFixtureValue(params, name, env) {
|
|
15695
|
+
if (!params)
|
|
15696
|
+
return;
|
|
15697
|
+
const routeFixtures = isRecord2(params["routeFixtures"]) ? params["routeFixtures"] : {};
|
|
15698
|
+
const raw = readString(routeFixtures[name]) ?? readString(params[name]);
|
|
15699
|
+
return raw ? resolveReference(raw, env) : undefined;
|
|
15700
|
+
}
|
|
15701
|
+
function envFixtureValue(name, env) {
|
|
15702
|
+
const candidates = [
|
|
15703
|
+
envNameForParam("TESTERS_ROUTE", name),
|
|
15704
|
+
envNameForParam("TESTERS_FIXTURE", name),
|
|
15705
|
+
envNameForParam("ALUMIA_FIXTURE", name),
|
|
15706
|
+
...PARAM_ENV_CANDIDATES[name] ?? []
|
|
15707
|
+
];
|
|
15708
|
+
for (const candidate of candidates) {
|
|
15709
|
+
const value = env[candidate]?.trim();
|
|
15710
|
+
if (value)
|
|
15711
|
+
return value;
|
|
15712
|
+
}
|
|
15713
|
+
return;
|
|
15714
|
+
}
|
|
15715
|
+
function defaultFixtureValue(name) {
|
|
15716
|
+
if (name === "orgSlug")
|
|
15717
|
+
return "test-org";
|
|
15718
|
+
if (name.toLowerCase().endsWith("slug")) {
|
|
15719
|
+
return `test-${name.replace(/Slug$/i, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase() || "slug"}`;
|
|
15720
|
+
}
|
|
15721
|
+
if (name === "id" || name.toLowerCase().endsWith("id"))
|
|
15722
|
+
return DEFAULT_UUID;
|
|
15723
|
+
if (name.toLowerCase().includes("token"))
|
|
15724
|
+
return "test-token";
|
|
15725
|
+
return `test-${name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()}`;
|
|
15726
|
+
}
|
|
15727
|
+
function routeParamsFromPath(path) {
|
|
15728
|
+
if (!path)
|
|
15729
|
+
return [];
|
|
15730
|
+
const params = new Set;
|
|
15731
|
+
for (const match of path.matchAll(/:([A-Za-z0-9_]+)(?:\*\??)?/g)) {
|
|
15732
|
+
if (match[1])
|
|
15733
|
+
params.add(match[1]);
|
|
15734
|
+
}
|
|
15735
|
+
return [...params];
|
|
15736
|
+
}
|
|
15737
|
+
function resolveRouteFixtures(scenario, env = process.env) {
|
|
15738
|
+
const metadataParams = Array.isArray(scenario.metadata?.["fixtureParams"]) ? scenario.metadata["fixtureParams"].filter((value) => typeof value === "string") : [];
|
|
15739
|
+
const params = [...new Set([...metadataParams, ...routeParamsFromPath(scenario.targetPath)])];
|
|
15740
|
+
const values = {};
|
|
15741
|
+
const sources = {};
|
|
15742
|
+
const synthetic = [];
|
|
15743
|
+
for (const param of params) {
|
|
15744
|
+
const scenarioValue = scenarioFixtureValue(scenario.parameters, param, env);
|
|
15745
|
+
if (scenarioValue) {
|
|
15746
|
+
values[param] = scenarioValue;
|
|
15747
|
+
sources[param] = "scenario";
|
|
15748
|
+
continue;
|
|
15749
|
+
}
|
|
15750
|
+
const envValue = envFixtureValue(param, env);
|
|
15751
|
+
if (envValue) {
|
|
15752
|
+
values[param] = envValue;
|
|
15753
|
+
sources[param] = "env";
|
|
15754
|
+
continue;
|
|
15755
|
+
}
|
|
15756
|
+
values[param] = defaultFixtureValue(param);
|
|
15757
|
+
sources[param] = "default";
|
|
15758
|
+
synthetic.push(param);
|
|
15759
|
+
}
|
|
15760
|
+
const resolvedPath = scenario.targetPath ? resolveRoutePath(scenario.targetPath, values) : null;
|
|
15761
|
+
return {
|
|
15762
|
+
originalPath: scenario.targetPath,
|
|
15763
|
+
resolvedPath,
|
|
15764
|
+
params,
|
|
15765
|
+
values,
|
|
15766
|
+
sources,
|
|
15767
|
+
synthetic
|
|
15768
|
+
};
|
|
15769
|
+
}
|
|
15770
|
+
function resolveRoutePath(path, values) {
|
|
15771
|
+
return path.replace(/\/:([A-Za-z0-9_]+)\*\?/g, (_match, name) => {
|
|
15772
|
+
const value = values[name];
|
|
15773
|
+
return value ? `/${encodeRouteFixture(value, true)}` : "";
|
|
15774
|
+
}).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));
|
|
15775
|
+
}
|
|
15776
|
+
function encodeRouteFixture(value, allowSlash) {
|
|
15777
|
+
if (allowSlash)
|
|
15778
|
+
return value.split("/").filter(Boolean).map(encodeURIComponent).join("/");
|
|
15779
|
+
return encodeURIComponent(value);
|
|
15780
|
+
}
|
|
15781
|
+
function materializeScenarioRoute(scenario, env = process.env) {
|
|
15782
|
+
const resolution = resolveRouteFixtures(scenario, env);
|
|
15783
|
+
if (!resolution.resolvedPath || resolution.resolvedPath === scenario.targetPath) {
|
|
15784
|
+
return { scenario, resolution };
|
|
15785
|
+
}
|
|
15786
|
+
const steps = scenario.steps.map((step) => {
|
|
15787
|
+
let next = step;
|
|
15788
|
+
for (const [name, value] of Object.entries(resolution.values)) {
|
|
15789
|
+
next = next.replaceAll(`:${name}`, value).replaceAll(`[${name}]`, value).replaceAll(`{${name}}`, value);
|
|
15790
|
+
}
|
|
15791
|
+
return next.replaceAll(scenario.targetPath ?? "", resolution.resolvedPath ?? "");
|
|
15792
|
+
});
|
|
15793
|
+
return {
|
|
15794
|
+
scenario: {
|
|
15795
|
+
...scenario,
|
|
15796
|
+
targetPath: resolution.resolvedPath,
|
|
15797
|
+
steps,
|
|
15798
|
+
metadata: {
|
|
15799
|
+
...scenario.metadata ?? {},
|
|
15800
|
+
routeFixtureResolution: resolution
|
|
15801
|
+
}
|
|
15802
|
+
},
|
|
15803
|
+
resolution
|
|
15804
|
+
};
|
|
15805
|
+
}
|
|
15806
|
+
function resolveStartUrl(baseUrl, targetPath) {
|
|
15807
|
+
try {
|
|
15808
|
+
return new URL(targetPath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
15809
|
+
} catch {
|
|
15810
|
+
return `${baseUrl.replace(/\/+$/, "")}/${targetPath.replace(/^\/+/, "")}`;
|
|
15811
|
+
}
|
|
15812
|
+
}
|
|
15813
|
+
var DEFAULT_UUID = "00000000-0000-4000-8000-000000000000", PARAM_ENV_CANDIDATES;
|
|
15814
|
+
var init_route_fixtures = __esm(() => {
|
|
15815
|
+
PARAM_ENV_CANDIDATES = {
|
|
15816
|
+
orgSlug: ["TESTERS_ORG_SLUG", "SMOKE_ORG_SLUG", "ORG_SLUG"],
|
|
15817
|
+
orgId: ["TESTERS_ORG_ID", "SMOKE_ORG_ID", "ORG_ID"],
|
|
15818
|
+
projectSlug: ["TESTERS_PROJECT_SLUG", "SMOKE_PROJECT_SLUG", "PROJECT_SLUG"],
|
|
15819
|
+
projectId: ["TESTERS_PROJECT_ID", "SMOKE_PROJECT_ID", "PROJECT_ID"],
|
|
15820
|
+
workspaceId: ["TESTERS_WORKSPACE_ID", "SMOKE_WORKSPACE_ID", "WORKSPACE_ID"],
|
|
15821
|
+
agentId: ["TESTERS_AGENT_ID", "SMOKE_AGENT_ID", "AGENT_ID"],
|
|
15822
|
+
sessionId: ["TESTERS_SESSION_ID", "SMOKE_SESSION_ID", "SESSION_ID"],
|
|
15823
|
+
userId: ["TESTERS_USER_ID", "SMOKE_USER_ID", "USER_ID"]
|
|
15824
|
+
};
|
|
15825
|
+
});
|
|
15826
|
+
|
|
15677
15827
|
// src/lib/browser-lightpanda.ts
|
|
15678
15828
|
var exports_browser_lightpanda = {};
|
|
15679
15829
|
__export(exports_browser_lightpanda, {
|
|
@@ -17407,33 +17557,35 @@ ${filtered.join(`
|
|
|
17407
17557
|
return { result: `Error executing ${toolName}: ${message}` };
|
|
17408
17558
|
}
|
|
17409
17559
|
}
|
|
17410
|
-
function resolveStartUrl(baseUrl, targetPath) {
|
|
17411
|
-
try {
|
|
17412
|
-
return new URL(targetPath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
17413
|
-
} catch {
|
|
17414
|
-
return `${baseUrl.replace(/\/+$/, "")}/${targetPath.replace(/^\/+/, "")}`;
|
|
17415
|
-
}
|
|
17416
|
-
}
|
|
17417
17560
|
function buildScenarioUserMessage(scenario, baseUrl) {
|
|
17561
|
+
const { scenario: materializedScenario, resolution } = materializeScenarioRoute(scenario);
|
|
17418
17562
|
const userParts = [
|
|
17419
|
-
`**Scenario:** ${
|
|
17420
|
-
`**Description:** ${
|
|
17563
|
+
`**Scenario:** ${materializedScenario.name}`,
|
|
17564
|
+
`**Description:** ${materializedScenario.description}`
|
|
17421
17565
|
];
|
|
17422
17566
|
if (baseUrl) {
|
|
17423
17567
|
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
17424
17568
|
userParts.push(`**Base URL:** ${normalizedBaseUrl}`);
|
|
17425
|
-
if (
|
|
17426
|
-
userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl,
|
|
17569
|
+
if (materializedScenario.targetPath) {
|
|
17570
|
+
userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl, materializedScenario.targetPath)}`);
|
|
17427
17571
|
}
|
|
17428
17572
|
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.");
|
|
17429
17573
|
}
|
|
17430
|
-
if (
|
|
17431
|
-
userParts.push(`**Target Path:** ${
|
|
17574
|
+
if (materializedScenario.targetPath) {
|
|
17575
|
+
userParts.push(`**Target Path:** ${materializedScenario.targetPath}`);
|
|
17576
|
+
}
|
|
17577
|
+
if (resolution.params.length > 0) {
|
|
17578
|
+
userParts.push("**Route Fixtures:**");
|
|
17579
|
+
for (const param of resolution.params) {
|
|
17580
|
+
const source = resolution.sources[param];
|
|
17581
|
+
const synthetic = source === "default" ? " synthetic" : "";
|
|
17582
|
+
userParts.push(`- :${param} = ${resolution.values[param]} (${source}${synthetic})`);
|
|
17583
|
+
}
|
|
17432
17584
|
}
|
|
17433
|
-
if (
|
|
17585
|
+
if (materializedScenario.steps.length > 0) {
|
|
17434
17586
|
userParts.push("**Steps:**");
|
|
17435
|
-
for (let i = 0;i <
|
|
17436
|
-
userParts.push(`${i + 1}. ${
|
|
17587
|
+
for (let i = 0;i < materializedScenario.steps.length; i++) {
|
|
17588
|
+
userParts.push(`${i + 1}. ${materializedScenario.steps[i]}`);
|
|
17437
17589
|
}
|
|
17438
17590
|
}
|
|
17439
17591
|
return userParts.join(`
|
|
@@ -17731,6 +17883,7 @@ function createClientForModel(model, apiKey) {
|
|
|
17731
17883
|
var activeHARs, activeCoverage, BROWSER_TOOLS;
|
|
17732
17884
|
var init_ai_client = __esm(() => {
|
|
17733
17885
|
init_types3();
|
|
17886
|
+
init_route_fixtures();
|
|
17734
17887
|
activeHARs = new Map;
|
|
17735
17888
|
activeCoverage = new Map;
|
|
17736
17889
|
BROWSER_TOOLS = [
|
|
@@ -21370,9 +21523,10 @@ function withTimeout(promise, ms, label) {
|
|
|
21370
21523
|
});
|
|
21371
21524
|
}
|
|
21372
21525
|
async function runSingleScenario(scenario, runId, options) {
|
|
21373
|
-
const
|
|
21526
|
+
const { scenario: materializedScenario, resolution: routeFixtureResolution } = materializeScenarioRoute(scenario);
|
|
21527
|
+
const scenarioType = materializedScenario.scenarioType ?? "browser";
|
|
21374
21528
|
if (scenarioType === "eval") {
|
|
21375
|
-
return runEvalScenario(
|
|
21529
|
+
return runEvalScenario(materializedScenario, { runId, baseUrl: options.url });
|
|
21376
21530
|
}
|
|
21377
21531
|
const config = loadConfig();
|
|
21378
21532
|
if (options.selfHeal !== undefined)
|
|
@@ -21413,7 +21567,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
21413
21567
|
runId,
|
|
21414
21568
|
scenarioId: scenario.id,
|
|
21415
21569
|
model,
|
|
21416
|
-
stepsTotal:
|
|
21570
|
+
stepsTotal: materializedScenario.steps.length || 10,
|
|
21417
21571
|
personaId: persona?.id ?? null,
|
|
21418
21572
|
personaName: persona?.name ?? null
|
|
21419
21573
|
});
|
|
@@ -21449,12 +21603,12 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
21449
21603
|
engine: effectiveOptions.engine
|
|
21450
21604
|
});
|
|
21451
21605
|
}
|
|
21452
|
-
const targetUrl =
|
|
21453
|
-
const scenarioTimeout =
|
|
21606
|
+
const targetUrl = materializedScenario.targetPath ? resolveStartUrl(options.url.replace(/\/$/, ""), materializedScenario.targetPath) : options.url;
|
|
21607
|
+
const scenarioTimeout = materializedScenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
|
|
21454
21608
|
registerSession({
|
|
21455
21609
|
resultId: result.id,
|
|
21456
21610
|
runId,
|
|
21457
|
-
scenarioId:
|
|
21611
|
+
scenarioId: materializedScenario.id,
|
|
21458
21612
|
engine: effectiveOptions.engine ?? "playwright",
|
|
21459
21613
|
startUrl: targetUrl
|
|
21460
21614
|
});
|
|
@@ -21508,7 +21662,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
21508
21662
|
const agentResult = await withTimeout(runAgentLoop({
|
|
21509
21663
|
client,
|
|
21510
21664
|
page,
|
|
21511
|
-
scenario,
|
|
21665
|
+
scenario: materializedScenario,
|
|
21512
21666
|
screenshotter,
|
|
21513
21667
|
model,
|
|
21514
21668
|
runId,
|
|
@@ -21599,7 +21753,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
21599
21753
|
const baseReasoning = agentResult.reasoning ? agentResult.reasoning + lightpandaNote : lightpandaNote || "";
|
|
21600
21754
|
const assertionOutcome = await applyStructuredAssertionsToResult({
|
|
21601
21755
|
page,
|
|
21602
|
-
scenario,
|
|
21756
|
+
scenario: materializedScenario,
|
|
21603
21757
|
consoleErrors,
|
|
21604
21758
|
status: agentResult.status,
|
|
21605
21759
|
reasoning: baseReasoning
|
|
@@ -21620,6 +21774,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
21620
21774
|
costCents: estimateCost(model, agentResult.tokensUsed),
|
|
21621
21775
|
metadata: {
|
|
21622
21776
|
consoleLogs,
|
|
21777
|
+
...routeFixtureResolution.params.length > 0 ? { routeFixtureResolution } : {},
|
|
21623
21778
|
...networkErrors.length > 0 ? networkMeta : {},
|
|
21624
21779
|
...structuredAssertionMeta
|
|
21625
21780
|
}
|
|
@@ -21969,6 +22124,7 @@ var init_runner = __esm(() => {
|
|
|
21969
22124
|
init_browser();
|
|
21970
22125
|
init_screenshotter();
|
|
21971
22126
|
init_ai_client();
|
|
22127
|
+
init_route_fixtures();
|
|
21972
22128
|
init_config2();
|
|
21973
22129
|
init_persona_auth();
|
|
21974
22130
|
init_session_tracker();
|
package/dist/server/index.js
CHANGED
|
@@ -14410,6 +14410,156 @@ var init_results = __esm(() => {
|
|
|
14410
14410
|
init_database();
|
|
14411
14411
|
});
|
|
14412
14412
|
|
|
14413
|
+
// src/lib/route-fixtures.ts
|
|
14414
|
+
function isRecord2(value) {
|
|
14415
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14416
|
+
}
|
|
14417
|
+
function envNameForParam(prefix, param) {
|
|
14418
|
+
return `${prefix}_${param.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}`;
|
|
14419
|
+
}
|
|
14420
|
+
function readString(value) {
|
|
14421
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
14422
|
+
}
|
|
14423
|
+
function resolveReference(value, env) {
|
|
14424
|
+
if (value.startsWith("$?"))
|
|
14425
|
+
return env[value.slice(2)]?.trim() || undefined;
|
|
14426
|
+
if (value.startsWith("$"))
|
|
14427
|
+
return env[value.slice(1)]?.trim() || undefined;
|
|
14428
|
+
return value;
|
|
14429
|
+
}
|
|
14430
|
+
function scenarioFixtureValue(params, name, env) {
|
|
14431
|
+
if (!params)
|
|
14432
|
+
return;
|
|
14433
|
+
const routeFixtures = isRecord2(params["routeFixtures"]) ? params["routeFixtures"] : {};
|
|
14434
|
+
const raw = readString(routeFixtures[name]) ?? readString(params[name]);
|
|
14435
|
+
return raw ? resolveReference(raw, env) : undefined;
|
|
14436
|
+
}
|
|
14437
|
+
function envFixtureValue(name, env) {
|
|
14438
|
+
const candidates = [
|
|
14439
|
+
envNameForParam("TESTERS_ROUTE", name),
|
|
14440
|
+
envNameForParam("TESTERS_FIXTURE", name),
|
|
14441
|
+
envNameForParam("ALUMIA_FIXTURE", name),
|
|
14442
|
+
...PARAM_ENV_CANDIDATES[name] ?? []
|
|
14443
|
+
];
|
|
14444
|
+
for (const candidate of candidates) {
|
|
14445
|
+
const value = env[candidate]?.trim();
|
|
14446
|
+
if (value)
|
|
14447
|
+
return value;
|
|
14448
|
+
}
|
|
14449
|
+
return;
|
|
14450
|
+
}
|
|
14451
|
+
function defaultFixtureValue(name) {
|
|
14452
|
+
if (name === "orgSlug")
|
|
14453
|
+
return "test-org";
|
|
14454
|
+
if (name.toLowerCase().endsWith("slug")) {
|
|
14455
|
+
return `test-${name.replace(/Slug$/i, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase() || "slug"}`;
|
|
14456
|
+
}
|
|
14457
|
+
if (name === "id" || name.toLowerCase().endsWith("id"))
|
|
14458
|
+
return DEFAULT_UUID;
|
|
14459
|
+
if (name.toLowerCase().includes("token"))
|
|
14460
|
+
return "test-token";
|
|
14461
|
+
return `test-${name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()}`;
|
|
14462
|
+
}
|
|
14463
|
+
function routeParamsFromPath(path) {
|
|
14464
|
+
if (!path)
|
|
14465
|
+
return [];
|
|
14466
|
+
const params = new Set;
|
|
14467
|
+
for (const match of path.matchAll(/:([A-Za-z0-9_]+)(?:\*\??)?/g)) {
|
|
14468
|
+
if (match[1])
|
|
14469
|
+
params.add(match[1]);
|
|
14470
|
+
}
|
|
14471
|
+
return [...params];
|
|
14472
|
+
}
|
|
14473
|
+
function resolveRouteFixtures(scenario, env = process.env) {
|
|
14474
|
+
const metadataParams = Array.isArray(scenario.metadata?.["fixtureParams"]) ? scenario.metadata["fixtureParams"].filter((value) => typeof value === "string") : [];
|
|
14475
|
+
const params = [...new Set([...metadataParams, ...routeParamsFromPath(scenario.targetPath)])];
|
|
14476
|
+
const values = {};
|
|
14477
|
+
const sources = {};
|
|
14478
|
+
const synthetic = [];
|
|
14479
|
+
for (const param of params) {
|
|
14480
|
+
const scenarioValue = scenarioFixtureValue(scenario.parameters, param, env);
|
|
14481
|
+
if (scenarioValue) {
|
|
14482
|
+
values[param] = scenarioValue;
|
|
14483
|
+
sources[param] = "scenario";
|
|
14484
|
+
continue;
|
|
14485
|
+
}
|
|
14486
|
+
const envValue = envFixtureValue(param, env);
|
|
14487
|
+
if (envValue) {
|
|
14488
|
+
values[param] = envValue;
|
|
14489
|
+
sources[param] = "env";
|
|
14490
|
+
continue;
|
|
14491
|
+
}
|
|
14492
|
+
values[param] = defaultFixtureValue(param);
|
|
14493
|
+
sources[param] = "default";
|
|
14494
|
+
synthetic.push(param);
|
|
14495
|
+
}
|
|
14496
|
+
const resolvedPath = scenario.targetPath ? resolveRoutePath(scenario.targetPath, values) : null;
|
|
14497
|
+
return {
|
|
14498
|
+
originalPath: scenario.targetPath,
|
|
14499
|
+
resolvedPath,
|
|
14500
|
+
params,
|
|
14501
|
+
values,
|
|
14502
|
+
sources,
|
|
14503
|
+
synthetic
|
|
14504
|
+
};
|
|
14505
|
+
}
|
|
14506
|
+
function resolveRoutePath(path, values) {
|
|
14507
|
+
return path.replace(/\/:([A-Za-z0-9_]+)\*\?/g, (_match, name) => {
|
|
14508
|
+
const value = values[name];
|
|
14509
|
+
return value ? `/${encodeRouteFixture(value, true)}` : "";
|
|
14510
|
+
}).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));
|
|
14511
|
+
}
|
|
14512
|
+
function encodeRouteFixture(value, allowSlash) {
|
|
14513
|
+
if (allowSlash)
|
|
14514
|
+
return value.split("/").filter(Boolean).map(encodeURIComponent).join("/");
|
|
14515
|
+
return encodeURIComponent(value);
|
|
14516
|
+
}
|
|
14517
|
+
function materializeScenarioRoute(scenario, env = process.env) {
|
|
14518
|
+
const resolution = resolveRouteFixtures(scenario, env);
|
|
14519
|
+
if (!resolution.resolvedPath || resolution.resolvedPath === scenario.targetPath) {
|
|
14520
|
+
return { scenario, resolution };
|
|
14521
|
+
}
|
|
14522
|
+
const steps = scenario.steps.map((step) => {
|
|
14523
|
+
let next = step;
|
|
14524
|
+
for (const [name, value] of Object.entries(resolution.values)) {
|
|
14525
|
+
next = next.replaceAll(`:${name}`, value).replaceAll(`[${name}]`, value).replaceAll(`{${name}}`, value);
|
|
14526
|
+
}
|
|
14527
|
+
return next.replaceAll(scenario.targetPath ?? "", resolution.resolvedPath ?? "");
|
|
14528
|
+
});
|
|
14529
|
+
return {
|
|
14530
|
+
scenario: {
|
|
14531
|
+
...scenario,
|
|
14532
|
+
targetPath: resolution.resolvedPath,
|
|
14533
|
+
steps,
|
|
14534
|
+
metadata: {
|
|
14535
|
+
...scenario.metadata ?? {},
|
|
14536
|
+
routeFixtureResolution: resolution
|
|
14537
|
+
}
|
|
14538
|
+
},
|
|
14539
|
+
resolution
|
|
14540
|
+
};
|
|
14541
|
+
}
|
|
14542
|
+
function resolveStartUrl(baseUrl, targetPath) {
|
|
14543
|
+
try {
|
|
14544
|
+
return new URL(targetPath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
14545
|
+
} catch {
|
|
14546
|
+
return `${baseUrl.replace(/\/+$/, "")}/${targetPath.replace(/^\/+/, "")}`;
|
|
14547
|
+
}
|
|
14548
|
+
}
|
|
14549
|
+
var DEFAULT_UUID = "00000000-0000-4000-8000-000000000000", PARAM_ENV_CANDIDATES;
|
|
14550
|
+
var init_route_fixtures = __esm(() => {
|
|
14551
|
+
PARAM_ENV_CANDIDATES = {
|
|
14552
|
+
orgSlug: ["TESTERS_ORG_SLUG", "SMOKE_ORG_SLUG", "ORG_SLUG"],
|
|
14553
|
+
orgId: ["TESTERS_ORG_ID", "SMOKE_ORG_ID", "ORG_ID"],
|
|
14554
|
+
projectSlug: ["TESTERS_PROJECT_SLUG", "SMOKE_PROJECT_SLUG", "PROJECT_SLUG"],
|
|
14555
|
+
projectId: ["TESTERS_PROJECT_ID", "SMOKE_PROJECT_ID", "PROJECT_ID"],
|
|
14556
|
+
workspaceId: ["TESTERS_WORKSPACE_ID", "SMOKE_WORKSPACE_ID", "WORKSPACE_ID"],
|
|
14557
|
+
agentId: ["TESTERS_AGENT_ID", "SMOKE_AGENT_ID", "AGENT_ID"],
|
|
14558
|
+
sessionId: ["TESTERS_SESSION_ID", "SMOKE_SESSION_ID", "SESSION_ID"],
|
|
14559
|
+
userId: ["TESTERS_USER_ID", "SMOKE_USER_ID", "USER_ID"]
|
|
14560
|
+
};
|
|
14561
|
+
});
|
|
14562
|
+
|
|
14413
14563
|
// src/lib/browser-lightpanda.ts
|
|
14414
14564
|
var exports_browser_lightpanda = {};
|
|
14415
14565
|
__export(exports_browser_lightpanda, {
|
|
@@ -16094,33 +16244,35 @@ ${filtered.join(`
|
|
|
16094
16244
|
return { result: `Error executing ${toolName}: ${message}` };
|
|
16095
16245
|
}
|
|
16096
16246
|
}
|
|
16097
|
-
function resolveStartUrl(baseUrl, targetPath) {
|
|
16098
|
-
try {
|
|
16099
|
-
return new URL(targetPath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
16100
|
-
} catch {
|
|
16101
|
-
return `${baseUrl.replace(/\/+$/, "")}/${targetPath.replace(/^\/+/, "")}`;
|
|
16102
|
-
}
|
|
16103
|
-
}
|
|
16104
16247
|
function buildScenarioUserMessage(scenario, baseUrl) {
|
|
16248
|
+
const { scenario: materializedScenario, resolution } = materializeScenarioRoute(scenario);
|
|
16105
16249
|
const userParts = [
|
|
16106
|
-
`**Scenario:** ${
|
|
16107
|
-
`**Description:** ${
|
|
16250
|
+
`**Scenario:** ${materializedScenario.name}`,
|
|
16251
|
+
`**Description:** ${materializedScenario.description}`
|
|
16108
16252
|
];
|
|
16109
16253
|
if (baseUrl) {
|
|
16110
16254
|
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
16111
16255
|
userParts.push(`**Base URL:** ${normalizedBaseUrl}`);
|
|
16112
|
-
if (
|
|
16113
|
-
userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl,
|
|
16256
|
+
if (materializedScenario.targetPath) {
|
|
16257
|
+
userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl, materializedScenario.targetPath)}`);
|
|
16114
16258
|
}
|
|
16115
16259
|
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.");
|
|
16116
16260
|
}
|
|
16117
|
-
if (
|
|
16118
|
-
userParts.push(`**Target Path:** ${
|
|
16261
|
+
if (materializedScenario.targetPath) {
|
|
16262
|
+
userParts.push(`**Target Path:** ${materializedScenario.targetPath}`);
|
|
16263
|
+
}
|
|
16264
|
+
if (resolution.params.length > 0) {
|
|
16265
|
+
userParts.push("**Route Fixtures:**");
|
|
16266
|
+
for (const param of resolution.params) {
|
|
16267
|
+
const source = resolution.sources[param];
|
|
16268
|
+
const synthetic = source === "default" ? " synthetic" : "";
|
|
16269
|
+
userParts.push(`- :${param} = ${resolution.values[param]} (${source}${synthetic})`);
|
|
16270
|
+
}
|
|
16119
16271
|
}
|
|
16120
|
-
if (
|
|
16272
|
+
if (materializedScenario.steps.length > 0) {
|
|
16121
16273
|
userParts.push("**Steps:**");
|
|
16122
|
-
for (let i = 0;i <
|
|
16123
|
-
userParts.push(`${i + 1}. ${
|
|
16274
|
+
for (let i = 0;i < materializedScenario.steps.length; i++) {
|
|
16275
|
+
userParts.push(`${i + 1}. ${materializedScenario.steps[i]}`);
|
|
16124
16276
|
}
|
|
16125
16277
|
}
|
|
16126
16278
|
return userParts.join(`
|
|
@@ -16418,6 +16570,7 @@ function createClientForModel(model, apiKey) {
|
|
|
16418
16570
|
var activeHARs, activeCoverage, BROWSER_TOOLS;
|
|
16419
16571
|
var init_ai_client = __esm(() => {
|
|
16420
16572
|
init_types2();
|
|
16573
|
+
init_route_fixtures();
|
|
16421
16574
|
activeHARs = new Map;
|
|
16422
16575
|
activeCoverage = new Map;
|
|
16423
16576
|
BROWSER_TOOLS = [
|
|
@@ -46937,7 +47090,7 @@ import { join as join14 } from "path";
|
|
|
46937
47090
|
// package.json
|
|
46938
47091
|
var package_default = {
|
|
46939
47092
|
name: "@hasna/testers",
|
|
46940
|
-
version: "0.0.
|
|
47093
|
+
version: "0.0.52",
|
|
46941
47094
|
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
46942
47095
|
type: "module",
|
|
46943
47096
|
main: "dist/index.js",
|
|
@@ -48526,6 +48679,7 @@ class Screenshotter {
|
|
|
48526
48679
|
|
|
48527
48680
|
// src/lib/runner.ts
|
|
48528
48681
|
init_ai_client();
|
|
48682
|
+
init_route_fixtures();
|
|
48529
48683
|
init_config2();
|
|
48530
48684
|
|
|
48531
48685
|
// src/lib/secrets-resolver.ts
|
|
@@ -49572,9 +49726,10 @@ function withTimeout(promise, ms, label) {
|
|
|
49572
49726
|
});
|
|
49573
49727
|
}
|
|
49574
49728
|
async function runSingleScenario(scenario, runId, options) {
|
|
49575
|
-
const
|
|
49729
|
+
const { scenario: materializedScenario, resolution: routeFixtureResolution } = materializeScenarioRoute(scenario);
|
|
49730
|
+
const scenarioType = materializedScenario.scenarioType ?? "browser";
|
|
49576
49731
|
if (scenarioType === "eval") {
|
|
49577
|
-
return runEvalScenario(
|
|
49732
|
+
return runEvalScenario(materializedScenario, { runId, baseUrl: options.url });
|
|
49578
49733
|
}
|
|
49579
49734
|
const config = loadConfig();
|
|
49580
49735
|
if (options.selfHeal !== undefined)
|
|
@@ -49615,7 +49770,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
49615
49770
|
runId,
|
|
49616
49771
|
scenarioId: scenario.id,
|
|
49617
49772
|
model,
|
|
49618
|
-
stepsTotal:
|
|
49773
|
+
stepsTotal: materializedScenario.steps.length || 10,
|
|
49619
49774
|
personaId: persona?.id ?? null,
|
|
49620
49775
|
personaName: persona?.name ?? null
|
|
49621
49776
|
});
|
|
@@ -49651,12 +49806,12 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
49651
49806
|
engine: effectiveOptions.engine
|
|
49652
49807
|
});
|
|
49653
49808
|
}
|
|
49654
|
-
const targetUrl =
|
|
49655
|
-
const scenarioTimeout =
|
|
49809
|
+
const targetUrl = materializedScenario.targetPath ? resolveStartUrl(options.url.replace(/\/$/, ""), materializedScenario.targetPath) : options.url;
|
|
49810
|
+
const scenarioTimeout = materializedScenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
|
|
49656
49811
|
registerSession({
|
|
49657
49812
|
resultId: result.id,
|
|
49658
49813
|
runId,
|
|
49659
|
-
scenarioId:
|
|
49814
|
+
scenarioId: materializedScenario.id,
|
|
49660
49815
|
engine: effectiveOptions.engine ?? "playwright",
|
|
49661
49816
|
startUrl: targetUrl
|
|
49662
49817
|
});
|
|
@@ -49710,7 +49865,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
49710
49865
|
const agentResult = await withTimeout(runAgentLoop({
|
|
49711
49866
|
client,
|
|
49712
49867
|
page,
|
|
49713
|
-
scenario,
|
|
49868
|
+
scenario: materializedScenario,
|
|
49714
49869
|
screenshotter,
|
|
49715
49870
|
model,
|
|
49716
49871
|
runId,
|
|
@@ -49801,7 +49956,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
49801
49956
|
const baseReasoning = agentResult.reasoning ? agentResult.reasoning + lightpandaNote : lightpandaNote || "";
|
|
49802
49957
|
const assertionOutcome = await applyStructuredAssertionsToResult({
|
|
49803
49958
|
page,
|
|
49804
|
-
scenario,
|
|
49959
|
+
scenario: materializedScenario,
|
|
49805
49960
|
consoleErrors,
|
|
49806
49961
|
status: agentResult.status,
|
|
49807
49962
|
reasoning: baseReasoning
|
|
@@ -49822,6 +49977,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
49822
49977
|
costCents: estimateCost(model, agentResult.tokensUsed),
|
|
49823
49978
|
metadata: {
|
|
49824
49979
|
consoleLogs,
|
|
49980
|
+
...routeFixtureResolution.params.length > 0 ? { routeFixtureResolution } : {},
|
|
49825
49981
|
...networkErrors.length > 0 ? networkMeta : {},
|
|
49826
49982
|
...structuredAssertionMeta
|
|
49827
49983
|
}
|