@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/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.49",
55
+ version: "0.0.51",
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:** ${scenario.name}`,
17420
- `**Description:** ${scenario.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 (scenario.targetPath) {
17426
- userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl, scenario.targetPath)}`);
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 (scenario.targetPath) {
17431
- userParts.push(`**Target Path:** ${scenario.targetPath}`);
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 (scenario.steps.length > 0) {
17585
+ if (materializedScenario.steps.length > 0) {
17434
17586
  userParts.push("**Steps:**");
17435
- for (let i = 0;i < scenario.steps.length; i++) {
17436
- userParts.push(`${i + 1}. ${scenario.steps[i]}`);
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 scenarioType = scenario.scenarioType ?? "browser";
21526
+ const { scenario: materializedScenario, resolution: routeFixtureResolution } = materializeScenarioRoute(scenario);
21527
+ const scenarioType = materializedScenario.scenarioType ?? "browser";
21374
21528
  if (scenarioType === "eval") {
21375
- return runEvalScenario(scenario, { runId, baseUrl: options.url });
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: scenario.steps.length || 10,
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 = scenario.targetPath ? `${options.url.replace(/\/$/, "")}${scenario.targetPath}` : options.url;
21453
- const scenarioTimeout = scenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
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: scenario.id,
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();
@@ -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:** ${scenario.name}`,
16107
- `**Description:** ${scenario.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 (scenario.targetPath) {
16113
- userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl, scenario.targetPath)}`);
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 (scenario.targetPath) {
16118
- userParts.push(`**Target Path:** ${scenario.targetPath}`);
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 (scenario.steps.length > 0) {
16272
+ if (materializedScenario.steps.length > 0) {
16121
16273
  userParts.push("**Steps:**");
16122
- for (let i = 0;i < scenario.steps.length; i++) {
16123
- userParts.push(`${i + 1}. ${scenario.steps[i]}`);
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.49",
47093
+ version: "0.0.51",
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 scenarioType = scenario.scenarioType ?? "browser";
49729
+ const { scenario: materializedScenario, resolution: routeFixtureResolution } = materializeScenarioRoute(scenario);
49730
+ const scenarioType = materializedScenario.scenarioType ?? "browser";
49576
49731
  if (scenarioType === "eval") {
49577
- return runEvalScenario(scenario, { runId, baseUrl: options.url });
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: scenario.steps.length || 10,
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 = scenario.targetPath ? `${options.url.replace(/\/$/, "")}${scenario.targetPath}` : options.url;
49655
- const scenarioTimeout = scenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
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: scenario.id,
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/testers",
3
- "version": "0.0.49",
3
+ "version": "0.0.51",
4
4
  "description": "AI-powered QA testing CLI — spawns cheap AI agents to test web apps with headless browsers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",