@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 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:** ${scenario.name}`,
14790
- `**Description:** ${scenario.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 (scenario.targetPath) {
14796
- userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl, scenario.targetPath)}`);
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 (scenario.targetPath) {
14801
- userParts.push(`**Target Path:** ${scenario.targetPath}`);
14966
+ if (materializedScenario.targetPath) {
14967
+ userParts.push(`**Target Path:** ${materializedScenario.targetPath}`);
14802
14968
  }
14803
- if (scenario.steps.length > 0) {
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 < scenario.steps.length; i++) {
14806
- userParts.push(`${i + 1}. ${scenario.steps[i]}`);
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 scenarioType = scenario.scenarioType ?? "browser";
18499
+ const { scenario: materializedScenario, resolution: routeFixtureResolution } = materializeScenarioRoute(scenario);
18500
+ const scenarioType = materializedScenario.scenarioType ?? "browser";
18325
18501
  if (scenarioType === "eval") {
18326
- return runEvalScenario(scenario, { runId, baseUrl: options.url });
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: scenario.steps.length || 10,
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 = scenario.targetPath ? `${options.url.replace(/\/$/, "")}${scenario.targetPath}` : options.url;
18404
- const scenarioTimeout = scenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
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: scenario.id,
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 methods = kind === "api" ? extractRouteMethods(file) : [];
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(file) {
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.49",
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;AA6B3B,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,CAuDlG"}
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"}