@hasna/testers 0.0.48 → 0.0.49

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
@@ -59150,6 +59150,297 @@ var init_openapi_import = __esm(() => {
59150
59150
  init_api_checks();
59151
59151
  });
59152
59152
 
59153
+ // src/lib/next-route-inventory.ts
59154
+ var exports_next_route_inventory = {};
59155
+ __export(exports_next_route_inventory, {
59156
+ scenarioInputForNextRoute: () => scenarioInputForNextRoute,
59157
+ importNextRouteInventory: () => importNextRouteInventory,
59158
+ discoverNextRouteInventory: () => discoverNextRouteInventory
59159
+ });
59160
+ import { existsSync as existsSync18, readdirSync as readdirSync6, readFileSync as readFileSync9, statSync as statSync4 } from "fs";
59161
+ import { basename as basename3, join as join20, relative as relative4, resolve as resolve3 } from "path";
59162
+ function discoverNextRouteInventory(options) {
59163
+ const rootDir = resolve3(options.rootDir);
59164
+ const appDir = resolveAppDir(rootDir, options.appDir);
59165
+ const includePages = options.includePages !== false;
59166
+ const includeApi = options.includeApi !== false;
59167
+ const files = walkRouteFiles(appDir);
59168
+ const items = files.map((file) => routeItemFromFile(rootDir, appDir, file)).filter((item) => Boolean(item)).filter((item) => item.kind === "page" ? includePages : includeApi).sort((a2, b2) => `${a2.kind}:${a2.routePath}:${a2.file}`.localeCompare(`${b2.kind}:${b2.routePath}:${b2.file}`)).slice(0, options.limit);
59169
+ const categories = {};
59170
+ for (const item of items) {
59171
+ categories[item.category] = (categories[item.category] ?? 0) + 1;
59172
+ }
59173
+ return {
59174
+ rootDir,
59175
+ appDir,
59176
+ total: items.length,
59177
+ pages: items.filter((item) => item.kind === "page").length,
59178
+ apiRoutes: items.filter((item) => item.kind === "api").length,
59179
+ dynamic: items.filter((item) => item.dynamic).length,
59180
+ categories,
59181
+ items
59182
+ };
59183
+ }
59184
+ function scenarioInputForNextRoute(item, projectId) {
59185
+ const label = item.kind === "page" ? "page" : "API route";
59186
+ const methodList = item.methods.length > 0 ? item.methods.join(", ") : "discovered methods";
59187
+ const dynamicStep = item.dynamic ? "Substitute dynamic path parameters with valid fixture values from the target org before opening or calling the route." : undefined;
59188
+ const pageSteps = [
59189
+ dynamicStep,
59190
+ `Open the Next.js ${label} ${item.routePath}.`,
59191
+ "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.",
59193
+ "Verify the route stays within the expected org/workspace context and does not emit console errors."
59194
+ ].filter(Boolean);
59195
+ const apiSteps = [
59196
+ dynamicStep,
59197
+ `Call the ${methodList} handler(s) for ${item.routePath} using safe fixture data.`,
59198
+ "Verify expected authentication, authorization, validation, and tenant isolation behavior.",
59199
+ "For mutating methods, use harmless test payloads and confirm the response does not create cross-org side effects.",
59200
+ "Verify response status, JSON shape, and error messages are stable and regression-safe."
59201
+ ].filter(Boolean);
59202
+ return {
59203
+ name: `Next ${label}: ${item.routePath}`,
59204
+ description: `Source-discovered ${label} from ${item.file}. Verify route behavior and regressions for ${item.category}.`,
59205
+ steps: item.kind === "page" ? pageSteps : apiSteps,
59206
+ tags: item.tags,
59207
+ priority: item.priority,
59208
+ targetPath: item.routePath,
59209
+ requiresAuth: item.requiresAuth,
59210
+ assertions: item.kind === "page" ? SAFE_PAGE_ASSERTIONS : [],
59211
+ metadata: {
59212
+ source: "next-route-inventory",
59213
+ routeFile: item.file,
59214
+ routeKind: item.kind,
59215
+ category: item.category,
59216
+ methods: item.methods,
59217
+ dynamic: item.dynamic,
59218
+ groups: item.groups
59219
+ },
59220
+ projectId
59221
+ };
59222
+ }
59223
+ function importNextRouteInventory(options) {
59224
+ const inventory = discoverNextRouteInventory(options);
59225
+ let created = 0;
59226
+ let updated = 0;
59227
+ let deduped = 0;
59228
+ const scenarios = [];
59229
+ const workflows = [];
59230
+ if (options.createScenarios) {
59231
+ for (const item of inventory.items) {
59232
+ const result = upsertScenario(scenarioInputForNextRoute(item, options.projectId));
59233
+ scenarios.push(result.scenario);
59234
+ if (result.action === "created")
59235
+ created++;
59236
+ else if (result.action === "updated")
59237
+ updated++;
59238
+ else
59239
+ deduped++;
59240
+ }
59241
+ }
59242
+ if (options.createWorkflows) {
59243
+ workflows.push(...upsertRouteInventoryWorkflows(inventory, options));
59244
+ }
59245
+ return { inventory, created, updated, deduped, scenarios, workflows };
59246
+ }
59247
+ function upsertRouteInventoryWorkflows(inventory, options) {
59248
+ const workflows = [];
59249
+ const categories = Object.keys(inventory.categories).sort();
59250
+ for (const category of categories) {
59251
+ const kinds = new Set(inventory.items.filter((item) => item.category === category).map((item) => item.kind));
59252
+ for (const kind of kinds) {
59253
+ const name = `Next route inventory ${category} ${kind}`;
59254
+ const scenarioTags = ["next-route", `area:${category}`, `route:${kind}`];
59255
+ const execution = {
59256
+ target: options.workflowTarget ?? "sandbox",
59257
+ provider: options.workflowProvider,
59258
+ sandboxCleanup: "delete",
59259
+ sandboxSyncStrategy: "rsync",
59260
+ ...options.workflowExecution
59261
+ };
59262
+ const existing = listTestingWorkflows({ projectId: options.projectId, enabled: undefined }).find((workflow) => workflow.name === name);
59263
+ const input = {
59264
+ name,
59265
+ description: `Source-discovered Next.js ${kind} coverage for ${category} routes.`,
59266
+ projectId: options.projectId,
59267
+ scenarioFilter: { tags: scenarioTags },
59268
+ execution
59269
+ };
59270
+ workflows.push(existing ? updateTestingWorkflow(existing.id, input) : createTestingWorkflow(input));
59271
+ }
59272
+ }
59273
+ return workflows;
59274
+ }
59275
+ function resolveAppDir(rootDir, appDir) {
59276
+ const candidates = appDir ? [resolve3(rootDir, appDir)] : [
59277
+ join20(rootDir, "packages", "web", "app"),
59278
+ join20(rootDir, "app"),
59279
+ rootDir
59280
+ ];
59281
+ for (const candidate of candidates) {
59282
+ if (existsSync18(candidate) && statSync4(candidate).isDirectory())
59283
+ return candidate;
59284
+ }
59285
+ throw new Error(`Next.js app directory not found under ${rootDir}`);
59286
+ }
59287
+ function walkRouteFiles(appDir) {
59288
+ const files = [];
59289
+ function walk(dir) {
59290
+ for (const entry of readdirSync6(dir)) {
59291
+ if (WALK_EXCLUDES.has(entry))
59292
+ continue;
59293
+ const fullPath = join20(dir, entry);
59294
+ const stat = statSync4(fullPath);
59295
+ if (stat.isDirectory()) {
59296
+ walk(fullPath);
59297
+ } else if (ROUTE_FILE_NAMES.has(entry)) {
59298
+ files.push(fullPath);
59299
+ }
59300
+ }
59301
+ }
59302
+ walk(appDir);
59303
+ return files;
59304
+ }
59305
+ function routeItemFromFile(rootDir, appDir, file) {
59306
+ const fileName = basename3(file);
59307
+ const kind = fileName.startsWith("page.") ? "page" : "api";
59308
+ const relativeFile = relative4(rootDir, file);
59309
+ const appRelative = relative4(appDir, file).split(/[\\/]/);
59310
+ const routeSegments = appRelative.slice(0, -1);
59311
+ const groups = routeSegments.filter((segment) => segment.startsWith("(") && segment.endsWith(")")).map((segment) => segment.slice(1, -1));
59312
+ const pathSegments = routeSegments.filter((segment) => !segment.startsWith("(")).filter((segment) => !segment.startsWith("@")).map(normalizeRouteSegment).filter(Boolean);
59313
+ const routePath = `/${pathSegments.join("/")}`.replace(/\/+/g, "/");
59314
+ const normalizedRoutePath = routePath === "/" ? "/" : routePath.replace(/\/$/, "");
59315
+ const methods = kind === "api" ? extractRouteMethods(file) : [];
59316
+ const category = classifyRoute(normalizedRoutePath, groups, relativeFile);
59317
+ const dynamic = routeSegments.some((segment) => segment.includes("["));
59318
+ const requiresAuth = inferRequiresAuth(normalizedRoutePath, groups, kind);
59319
+ return {
59320
+ kind,
59321
+ routePath: normalizedRoutePath,
59322
+ file: relativeFile,
59323
+ category,
59324
+ groups,
59325
+ methods,
59326
+ dynamic,
59327
+ requiresAuth,
59328
+ tags: tagsForRoute({ kind, routePath: normalizedRoutePath, category, groups, dynamic, requiresAuth }),
59329
+ priority: priorityForRoute(normalizedRoutePath, category, kind)
59330
+ };
59331
+ }
59332
+ function normalizeRouteSegment(segment) {
59333
+ if (segment.startsWith("[[...") && segment.endsWith("]]")) {
59334
+ return `:${segment.slice(5, -2)}*?`;
59335
+ }
59336
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
59337
+ return `:${segment.slice(4, -1)}*`;
59338
+ }
59339
+ if (segment.startsWith("[") && segment.endsWith("]")) {
59340
+ return `:${segment.slice(1, -1)}`;
59341
+ }
59342
+ return segment;
59343
+ }
59344
+ function extractRouteMethods(file) {
59345
+ const source = readFileSync9(file, "utf8");
59346
+ const methods = new Set;
59347
+ 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
+ for (const match of source.matchAll(pattern)) {
59349
+ const method = match[1] ?? match[2];
59350
+ if (method)
59351
+ methods.add(method);
59352
+ }
59353
+ return [...methods].sort();
59354
+ }
59355
+ function classifyRoute(routePath, groups, file) {
59356
+ const haystack = `${routePath} ${groups.join(" ")} ${file}`.toLowerCase();
59357
+ if (haystack.includes("admin"))
59358
+ return "admin";
59359
+ if (haystack.includes("auth") || routePath.startsWith("/cli/device"))
59360
+ return "auth";
59361
+ if (haystack.includes("ai-runtime") || /\/(chat|sessions|memories|knowledge|learning|copilot|guardrails)\b/.test(routePath))
59362
+ return "ai-runtime";
59363
+ if (haystack.includes("commerce") || /\/(billing|shop|agent-wallet|domains|whois-profiles)\b/.test(routePath))
59364
+ return "commerce";
59365
+ if (haystack.includes("communications") || /\/(telephony|emails)\b/.test(routePath))
59366
+ return "communications";
59367
+ if (haystack.includes("crm") || routePath.includes("/contacts"))
59368
+ return "crm";
59369
+ if (haystack.includes("integrations") || routePath.includes("/connectors"))
59370
+ return "integrations";
59371
+ if (haystack.includes("dashboard") || routePath.includes(":orgSlug"))
59372
+ return "dashboard";
59373
+ if (haystack.includes("public") || haystack.includes("pages"))
59374
+ return "public";
59375
+ if (routePath.startsWith("/api/"))
59376
+ return "api";
59377
+ return "app";
59378
+ }
59379
+ function inferRequiresAuth(routePath, groups, kind) {
59380
+ const haystack = `${routePath} ${groups.join(" ")}`.toLowerCase();
59381
+ if (haystack.includes("auth") || haystack.includes("public") || haystack.includes("webhook"))
59382
+ return false;
59383
+ if (routePath.startsWith("/api/v1/auth/"))
59384
+ return false;
59385
+ if (routePath.startsWith("/api/"))
59386
+ return true;
59387
+ return kind === "page" && (haystack.includes("admin") || haystack.includes("dashboard") || routePath.includes(":orgSlug") || routePath.startsWith("/settings"));
59388
+ }
59389
+ function tagsForRoute(input) {
59390
+ const tags = new Set([
59391
+ "next-route",
59392
+ `route:${input.kind}`,
59393
+ `area:${input.category}`,
59394
+ input.category
59395
+ ]);
59396
+ for (const group of input.groups)
59397
+ tags.add(`group:${group}`);
59398
+ if (input.dynamic)
59399
+ tags.add("dynamic-route");
59400
+ if (input.requiresAuth)
59401
+ tags.add("auth-required");
59402
+ if (input.routePath.startsWith("/api/"))
59403
+ tags.add("api");
59404
+ return [...tags];
59405
+ }
59406
+ function priorityForRoute(routePath, category, kind) {
59407
+ if (category === "auth")
59408
+ return "critical";
59409
+ if (category === "commerce" || category === "ai-runtime")
59410
+ return "critical";
59411
+ if (category === "admin" || category === "dashboard")
59412
+ return "high";
59413
+ if (kind === "api")
59414
+ return "high";
59415
+ if (routePath === "/" || category === "public")
59416
+ return "medium";
59417
+ return "medium";
59418
+ }
59419
+ var ROUTE_FILE_NAMES, WALK_EXCLUDES, SAFE_PAGE_ASSERTIONS;
59420
+ var init_next_route_inventory = __esm(() => {
59421
+ init_scenarios();
59422
+ init_workflows();
59423
+ ROUTE_FILE_NAMES = new Set([
59424
+ "page.tsx",
59425
+ "page.ts",
59426
+ "page.jsx",
59427
+ "page.js",
59428
+ "page.mdx",
59429
+ "route.ts",
59430
+ "route.js"
59431
+ ]);
59432
+ WALK_EXCLUDES = new Set([
59433
+ ".git",
59434
+ ".next",
59435
+ ".turbo",
59436
+ "node_modules",
59437
+ "dist",
59438
+ "build",
59439
+ "coverage"
59440
+ ]);
59441
+ SAFE_PAGE_ASSERTIONS = [{ type: "no_console_errors" }];
59442
+ });
59443
+
59153
59444
  // src/lib/generator.ts
59154
59445
  var exports_generator = {};
59155
59446
  __export(exports_generator, {
@@ -59448,7 +59739,7 @@ async function recordSession(url, options) {
59448
59739
  await Promise.race([
59449
59740
  page.waitForEvent("close").catch(() => {}),
59450
59741
  context.waitForEvent("close").catch(() => {}),
59451
- new Promise((resolve3) => setTimeout(resolve3, timeout))
59742
+ new Promise((resolve4) => setTimeout(resolve4, timeout))
59452
59743
  ]);
59453
59744
  clearInterval(pollInterval);
59454
59745
  try {
@@ -77016,7 +77307,7 @@ function createProviderToolFactoryWithOutputSchema({
77016
77307
  supportsDeferredResults
77017
77308
  });
77018
77309
  }
77019
- async function resolve3(value) {
77310
+ async function resolve4(value) {
77020
77311
  if (typeof value === "function") {
77021
77312
  value = value();
77022
77313
  }
@@ -78703,7 +78994,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
78703
78994
  try {
78704
78995
  const { value } = await getFromApi({
78705
78996
  url: `${this.config.baseURL}/config`,
78706
- headers: await resolve3(this.config.headers()),
78997
+ headers: await resolve4(this.config.headers()),
78707
78998
  successfulResponseHandler: createJsonResponseHandler(gatewayAvailableModelsResponseSchema),
78708
78999
  failedResponseHandler: createJsonErrorResponseHandler({
78709
79000
  errorSchema: exports_external2.any(),
@@ -78721,7 +79012,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
78721
79012
  const baseUrl = new URL(this.config.baseURL);
78722
79013
  const { value } = await getFromApi({
78723
79014
  url: `${baseUrl.origin}/v1/credits`,
78724
- headers: await resolve3(this.config.headers()),
79015
+ headers: await resolve4(this.config.headers()),
78725
79016
  successfulResponseHandler: createJsonResponseHandler(gatewayCreditsResponseSchema),
78726
79017
  failedResponseHandler: createJsonErrorResponseHandler({
78727
79018
  errorSchema: exports_external2.any(),
@@ -78767,7 +79058,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
78767
79058
  }
78768
79059
  const { value } = await getFromApi({
78769
79060
  url: `${baseUrl.origin}/v1/report?${searchParams.toString()}`,
78770
- headers: await resolve3(this.config.headers()),
79061
+ headers: await resolve4(this.config.headers()),
78771
79062
  successfulResponseHandler: createJsonResponseHandler(gatewaySpendReportResponseSchema),
78772
79063
  failedResponseHandler: createJsonErrorResponseHandler({
78773
79064
  errorSchema: exports_external2.any(),
@@ -78789,7 +79080,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
78789
79080
  const baseUrl = new URL(this.config.baseURL);
78790
79081
  const { value } = await getFromApi({
78791
79082
  url: `${baseUrl.origin}/v1/generation?id=${encodeURIComponent(params.id)}`,
78792
- headers: await resolve3(this.config.headers()),
79083
+ headers: await resolve4(this.config.headers()),
78793
79084
  successfulResponseHandler: createJsonResponseHandler(gatewayGenerationInfoResponseSchema),
78794
79085
  failedResponseHandler: createJsonErrorResponseHandler({
78795
79086
  errorSchema: exports_external2.any(),
@@ -78822,7 +79113,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
78822
79113
  async doGenerate(options) {
78823
79114
  const { args, warnings } = await this.getArgs(options);
78824
79115
  const { abortSignal } = options;
78825
- const resolvedHeaders = await resolve3(this.config.headers());
79116
+ const resolvedHeaders = await resolve4(this.config.headers());
78826
79117
  try {
78827
79118
  const {
78828
79119
  responseHeaders,
@@ -78830,7 +79121,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
78830
79121
  rawValue: rawResponse
78831
79122
  } = await postJsonToApi({
78832
79123
  url: this.getUrl(),
78833
- headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, false), await resolve3(this.config.o11yHeaders)),
79124
+ headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, false), await resolve4(this.config.o11yHeaders)),
78834
79125
  body: args,
78835
79126
  successfulResponseHandler: createJsonResponseHandler(exports_external2.any()),
78836
79127
  failedResponseHandler: createJsonErrorResponseHandler({
@@ -78853,11 +79144,11 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
78853
79144
  async doStream(options) {
78854
79145
  const { args, warnings } = await this.getArgs(options);
78855
79146
  const { abortSignal } = options;
78856
- const resolvedHeaders = await resolve3(this.config.headers());
79147
+ const resolvedHeaders = await resolve4(this.config.headers());
78857
79148
  try {
78858
79149
  const { value: response, responseHeaders } = await postJsonToApi({
78859
79150
  url: this.getUrl(),
78860
- headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, true), await resolve3(this.config.o11yHeaders)),
79151
+ headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, true), await resolve4(this.config.o11yHeaders)),
78861
79152
  body: args,
78862
79153
  successfulResponseHandler: createEventSourceResponseHandler(exports_external2.any()),
78863
79154
  failedResponseHandler: createJsonErrorResponseHandler({
@@ -78942,7 +79233,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
78942
79233
  providerOptions
78943
79234
  }) {
78944
79235
  var _a92;
78945
- const resolvedHeaders = await resolve3(this.config.headers());
79236
+ const resolvedHeaders = await resolve4(this.config.headers());
78946
79237
  try {
78947
79238
  const {
78948
79239
  responseHeaders,
@@ -78950,7 +79241,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
78950
79241
  rawValue
78951
79242
  } = await postJsonToApi({
78952
79243
  url: this.getUrl(),
78953
- headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve3(this.config.o11yHeaders)),
79244
+ headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve4(this.config.o11yHeaders)),
78954
79245
  body: {
78955
79246
  values,
78956
79247
  ...providerOptions ? { providerOptions } : {}
@@ -79006,7 +79297,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
79006
79297
  abortSignal
79007
79298
  }) {
79008
79299
  var _a92, _b92, _c2, _d2;
79009
- const resolvedHeaders = await resolve3(this.config.headers());
79300
+ const resolvedHeaders = await resolve4(this.config.headers());
79010
79301
  try {
79011
79302
  const {
79012
79303
  responseHeaders,
@@ -79014,7 +79305,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
79014
79305
  rawValue
79015
79306
  } = await postJsonToApi({
79016
79307
  url: this.getUrl(),
79017
- headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve3(this.config.o11yHeaders)),
79308
+ headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve4(this.config.o11yHeaders)),
79018
79309
  body: {
79019
79310
  prompt,
79020
79311
  n: n2,
@@ -79089,11 +79380,11 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
79089
79380
  abortSignal
79090
79381
  }) {
79091
79382
  var _a92;
79092
- const resolvedHeaders = await resolve3(this.config.headers());
79383
+ const resolvedHeaders = await resolve4(this.config.headers());
79093
79384
  try {
79094
79385
  const { responseHeaders, value: responseBody } = await postJsonToApi({
79095
79386
  url: this.getUrl(),
79096
- headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve3(this.config.o11yHeaders), { accept: "text/event-stream" }),
79387
+ headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve4(this.config.o11yHeaders), { accept: "text/event-stream" }),
79097
79388
  body: {
79098
79389
  prompt,
79099
79390
  n: n2,
@@ -79216,7 +79507,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
79216
79507
  abortSignal,
79217
79508
  providerOptions
79218
79509
  }) {
79219
- const resolvedHeaders = await resolve3(this.config.headers());
79510
+ const resolvedHeaders = await resolve4(this.config.headers());
79220
79511
  try {
79221
79512
  const {
79222
79513
  responseHeaders,
@@ -79224,7 +79515,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
79224
79515
  rawValue
79225
79516
  } = await postJsonToApi({
79226
79517
  url: this.getUrl(),
79227
- headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve3(this.config.o11yHeaders)),
79518
+ headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve4(this.config.o11yHeaders)),
79228
79519
  body: {
79229
79520
  documents,
79230
79521
  query,
@@ -88642,7 +88933,7 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
88642
88933
  const schema = asSchema(inputSchema);
88643
88934
  return {
88644
88935
  name: "object",
88645
- responseFormat: resolve3(schema.jsonSchema).then((jsonSchema2) => ({
88936
+ responseFormat: resolve4(schema.jsonSchema).then((jsonSchema2) => ({
88646
88937
  type: "json",
88647
88938
  schema: jsonSchema2,
88648
88939
  ...name21 != null && { name: name21 },
@@ -88703,7 +88994,7 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
88703
88994
  const elementSchema = asSchema(inputElementSchema);
88704
88995
  return {
88705
88996
  name: "array",
88706
- responseFormat: resolve3(elementSchema.jsonSchema).then((jsonSchema2) => {
88997
+ responseFormat: resolve4(elementSchema.jsonSchema).then((jsonSchema2) => {
88707
88998
  const { $schema, ...itemSchema } = jsonSchema2;
88708
88999
  return {
88709
89000
  type: "json",
@@ -91600,9 +91891,9 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
91600
91891
  ...options
91601
91892
  }) {
91602
91893
  var _a21, _b16, _c2, _d2, _e2;
91603
- const resolvedBody = await resolve3(this.body);
91604
- const resolvedHeaders = await resolve3(this.headers);
91605
- const resolvedCredentials = await resolve3(this.credentials);
91894
+ const resolvedBody = await resolve4(this.body);
91895
+ const resolvedHeaders = await resolve4(this.headers);
91896
+ const resolvedCredentials = await resolve4(this.credentials);
91606
91897
  const baseHeaders = {
91607
91898
  ...normalizeHeaders(resolvedHeaders),
91608
91899
  ...normalizeHeaders(options.headers)
@@ -91650,9 +91941,9 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
91650
91941
  }
91651
91942
  async reconnectToStream(options) {
91652
91943
  var _a21, _b16, _c2, _d2, _e2;
91653
- const resolvedBody = await resolve3(this.body);
91654
- const resolvedHeaders = await resolve3(this.headers);
91655
- const resolvedCredentials = await resolve3(this.credentials);
91944
+ const resolvedBody = await resolve4(this.body);
91945
+ const resolvedHeaders = await resolve4(this.headers);
91946
+ const resolvedCredentials = await resolve4(this.credentials);
91656
91947
  const baseHeaders = {
91657
91948
  ...normalizeHeaders(resolvedHeaders),
91658
91949
  ...normalizeHeaders(options.headers)
@@ -93873,7 +94164,7 @@ __export(exports_session_converter, {
93873
94164
  convertSessionToScenario: () => convertSessionToScenario,
93874
94165
  convertSessionFile: () => convertSessionFile
93875
94166
  });
93876
- import { readFileSync as readFileSync9 } from "fs";
94167
+ import { readFileSync as readFileSync10 } from "fs";
93877
94168
  import { extname } from "path";
93878
94169
  function parseRrwebSession(events) {
93879
94170
  const result = [];
@@ -94058,7 +94349,7 @@ ${condensed}`;
94058
94349
  };
94059
94350
  }
94060
94351
  async function convertSessionFile(filePath, format, options) {
94061
- const raw = readFileSync9(filePath, "utf-8");
94352
+ const raw = readFileSync10(filePath, "utf-8");
94062
94353
  let parsed;
94063
94354
  try {
94064
94355
  parsed = JSON.parse(raw);
@@ -94092,7 +94383,7 @@ function detectSessionFormat(filePath) {
94092
94383
  if (ext === ".har")
94093
94384
  return "har";
94094
94385
  try {
94095
- const content = readFileSync9(filePath, "utf-8").trim();
94386
+ const content = readFileSync10(filePath, "utf-8").trim();
94096
94387
  const parsed = JSON.parse(content);
94097
94388
  if (Array.isArray(parsed) && parsed[0]?.type !== undefined && typeof parsed[0]?.timestamp === "number") {
94098
94389
  return "rrweb";
@@ -94536,7 +94827,7 @@ import chalk6 from "chalk";
94536
94827
  // package.json
94537
94828
  var package_default = {
94538
94829
  name: "@hasna/testers",
94539
- version: "0.0.48",
94830
+ version: "0.0.49",
94540
94831
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
94541
94832
  type: "module",
94542
94833
  main: "dist/index.js",
@@ -94635,9 +94926,9 @@ init_todos_connector();
94635
94926
  init_browser();
94636
94927
  import { render, Box, Text, useInput, useApp } from "ink";
94637
94928
  import React, { useState } from "react";
94638
- import { readFileSync as readFileSync10, readdirSync as readdirSync6, writeFileSync as writeFileSync7 } from "fs";
94929
+ import { readFileSync as readFileSync11, readdirSync as readdirSync7, writeFileSync as writeFileSync7 } from "fs";
94639
94930
  import { createInterface } from "readline";
94640
- import { join as join20, resolve as resolve4 } from "path";
94931
+ import { join as join21, resolve as resolve5 } from "path";
94641
94932
 
94642
94933
  // src/lib/init.ts
94643
94934
  init_paths();
@@ -96723,7 +97014,7 @@ init_ci();
96723
97014
  init_assertions();
96724
97015
  init_paths();
96725
97016
  init_sessions();
96726
- import { existsSync as existsSync18, mkdirSync as mkdirSync15 } from "fs";
97017
+ import { existsSync as existsSync19, mkdirSync as mkdirSync15 } from "fs";
96727
97018
 
96728
97019
  // src/lib/repo-discovery.ts
96729
97020
  init_paths();
@@ -97742,18 +98033,18 @@ program2.command("prod-debug <target>").description("Create a safe production de
97742
98033
  }, config2.prodDebug);
97743
98034
  const output = opts.json ? JSON.stringify(plan, null, 2) : formatProdDebugPlan(plan);
97744
98035
  if (opts.output) {
97745
- writeFileSync7(resolve4(opts.output), output + `
98036
+ writeFileSync7(resolve5(opts.output), output + `
97746
98037
  `);
97747
98038
  } else {
97748
98039
  log(output);
97749
98040
  }
97750
98041
  });
97751
98042
  var CONFIG_DIR5 = getTestersDir();
97752
- var CONFIG_PATH4 = join20(CONFIG_DIR5, "config.json");
98043
+ var CONFIG_PATH4 = join21(CONFIG_DIR5, "config.json");
97753
98044
  function getActiveProject() {
97754
98045
  try {
97755
- if (existsSync18(CONFIG_PATH4)) {
97756
- const raw = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
98046
+ if (existsSync19(CONFIG_PATH4)) {
98047
+ const raw = JSON.parse(readFileSync11(CONFIG_PATH4, "utf-8"));
97757
98048
  return raw.activeProject ?? undefined;
97758
98049
  }
97759
98050
  } catch {}
@@ -97960,7 +98251,7 @@ program2.command("delete <id>").description("Delete a scenario").option("-y, --y
97960
98251
  }
97961
98252
  if (!opts.yes) {
97962
98253
  process.stdout.write(chalk6.yellow(`Delete scenario ${scenario.shortId} "${scenario.name}"? [y/N] `));
97963
- const answer = await new Promise((resolve5) => {
98254
+ const answer = await new Promise((resolve6) => {
97964
98255
  let buf = "";
97965
98256
  process.stdin.setRawMode?.(true);
97966
98257
  process.stdin.resume();
@@ -97970,7 +98261,7 @@ program2.command("delete <id>").description("Delete a scenario").option("-y, --y
97970
98261
  process.stdin.pause();
97971
98262
  process.stdout.write(`
97972
98263
  `);
97973
- resolve5(buf);
98264
+ resolve6(buf);
97974
98265
  });
97975
98266
  });
97976
98267
  if (answer !== "y" && answer !== "yes") {
@@ -97999,7 +98290,7 @@ program2.command("remove <id>").alias("uninstall").description("Remove a scenari
97999
98290
  }
98000
98291
  if (!opts.yes) {
98001
98292
  process.stdout.write(chalk6.yellow(`Remove scenario ${scenario.shortId} "${scenario.name}"? [y/N] `));
98002
- const answer = await new Promise((resolve5) => {
98293
+ const answer = await new Promise((resolve6) => {
98003
98294
  let buf = "";
98004
98295
  process.stdin.setRawMode?.(true);
98005
98296
  process.stdin.resume();
@@ -98009,7 +98300,7 @@ program2.command("remove <id>").alias("uninstall").description("Remove a scenari
98009
98300
  process.stdin.pause();
98010
98301
  process.stdout.write(`
98011
98302
  `);
98012
- resolve5(buf);
98303
+ resolve6(buf);
98013
98304
  });
98014
98305
  });
98015
98306
  if (answer !== "y" && answer !== "yes") {
@@ -98212,7 +98503,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
98212
98503
  `);
98213
98504
  }
98214
98505
  };
98215
- await new Promise((resolve5) => {
98506
+ await new Promise((resolve6) => {
98216
98507
  const poll = setInterval(() => {
98217
98508
  const run3 = getRun(runId);
98218
98509
  if (!run3)
@@ -98220,7 +98511,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
98220
98511
  renderTable();
98221
98512
  if (DONE_STATUSES.has(run3.status)) {
98222
98513
  clearInterval(poll);
98223
- resolve5();
98514
+ resolve6();
98224
98515
  }
98225
98516
  }, POLL_INTERVAL);
98226
98517
  });
@@ -98615,15 +98906,15 @@ program2.command("screenshots <id>").description("List screenshots for a run or
98615
98906
  });
98616
98907
  program2.command("import <dir>").description("Import markdown test files as scenarios").action((dir) => {
98617
98908
  try {
98618
- const absDir = resolve4(dir);
98619
- const files = readdirSync6(absDir).filter((f2) => f2.endsWith(".md"));
98909
+ const absDir = resolve5(dir);
98910
+ const files = readdirSync7(absDir).filter((f2) => f2.endsWith(".md"));
98620
98911
  if (files.length === 0) {
98621
98912
  log(chalk6.dim("No .md files found in directory."));
98622
98913
  return;
98623
98914
  }
98624
98915
  let imported = 0;
98625
98916
  for (const file2 of files) {
98626
- const content = readFileSync10(join20(absDir, file2), "utf-8");
98917
+ const content = readFileSync11(join21(absDir, file2), "utf-8");
98627
98918
  const lines = content.split(`
98628
98919
  `);
98629
98920
  let name21 = file2.replace(/\.md$/, "");
@@ -98679,11 +98970,11 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
98679
98970
  const outputPath = opts.output ?? "testers-export.json";
98680
98971
  const data = JSON.stringify(scenarios, null, 2);
98681
98972
  writeFileSync7(outputPath, data, "utf-8");
98682
- log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${resolve4(outputPath)}`));
98973
+ log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${resolve5(outputPath)}`));
98683
98974
  return;
98684
98975
  }
98685
98976
  const outputDir = opts.output ?? ".";
98686
- if (!existsSync18(outputDir)) {
98977
+ if (!existsSync19(outputDir)) {
98687
98978
  mkdirSync15(outputDir, { recursive: true });
98688
98979
  }
98689
98980
  for (const s2 of scenarios) {
@@ -98711,13 +99002,13 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
98711
99002
  lines.push("");
98712
99003
  }
98713
99004
  const safeFilename = s2.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
98714
- const filePath = join20(outputDir, `${s2.shortId}-${safeFilename}.md`);
99005
+ const filePath = join21(outputDir, `${s2.shortId}-${safeFilename}.md`);
98715
99006
  writeFileSync7(filePath, lines.join(`
98716
99007
  `), "utf-8");
98717
99008
  log(chalk6.dim(` ${s2.shortId}: ${s2.name} \u2192 ${filePath}`));
98718
99009
  }
98719
99010
  log(chalk6.green(`
98720
- Exported ${scenarios.length} scenario(s) as markdown to ${resolve4(outputDir)}`));
99011
+ Exported ${scenarios.length} scenario(s) as markdown to ${resolve5(outputDir)}`));
98721
99012
  } catch (error40) {
98722
99013
  logError(chalk6.red(`Error: ${error40 instanceof Error ? error40.message : String(error40)}`));
98723
99014
  process.exit(1);
@@ -98736,7 +99027,7 @@ program2.command("status").description("Show database and auth status").action((
98736
99027
  try {
98737
99028
  const config2 = loadConfig();
98738
99029
  const hasApiKey = !!config2.anthropicApiKey || !!process.env["ANTHROPIC_API_KEY"];
98739
- const dbPath = join20(getTestersDir(), "testers.db");
99030
+ const dbPath = join21(getTestersDir(), "testers.db");
98740
99031
  log("");
98741
99032
  log(chalk6.bold(" Open Testers Status"));
98742
99033
  log("");
@@ -98886,13 +99177,13 @@ projectCmd.command("export-open <id>").description("Register a testers project i
98886
99177
  projectCmd.command("use <name>").description("Set active project (find or create)").option("--json", "Output as JSON", false).action((name21, opts) => {
98887
99178
  try {
98888
99179
  const project = ensureProject(name21, process.cwd());
98889
- if (!existsSync18(CONFIG_DIR5)) {
99180
+ if (!existsSync19(CONFIG_DIR5)) {
98890
99181
  mkdirSync15(CONFIG_DIR5, { recursive: true });
98891
99182
  }
98892
99183
  let config2 = {};
98893
- if (existsSync18(CONFIG_PATH4)) {
99184
+ if (existsSync19(CONFIG_PATH4)) {
98894
99185
  try {
98895
- config2 = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
99186
+ config2 = JSON.parse(readFileSync11(CONFIG_PATH4, "utf-8"));
98896
99187
  } catch {}
98897
99188
  }
98898
99189
  config2.activeProject = project.id;
@@ -98910,7 +99201,7 @@ projectCmd.command("use <name>").description("Set active project (find or create
98910
99201
  var repoCmd = program2.command("repo").description("Discover and run repo-native Playwright tests");
98911
99202
  repoCmd.command("discover [path]").alias("scan").description("Discover Playwright tests in a repo").option("--refresh", "Force a fresh scan, ignoring cache", false).option("--json", "Output as JSON", false).option("--base-url <url>", "Override the suggested base URL").action((path, opts) => {
98912
99203
  try {
98913
- const repoPath = resolve4(path ?? process.cwd());
99204
+ const repoPath = resolve5(path ?? process.cwd());
98914
99205
  const snapshot = discoverRepo({
98915
99206
  repoPath,
98916
99207
  refresh: opts.refresh,
@@ -98976,7 +99267,7 @@ repoCmd.command("discover [path]").alias("scan").description("Discover Playwrigh
98976
99267
  });
98977
99268
  repoCmd.command("prepare [path]").alias("prep").description("Install dependencies and browsers for repo tests").option("--all", "Run all prep steps (install, browsers, build, seed)", false).option("--install", "Install dependencies", false).option("--browsers", "Install Playwright browsers", false).option("--build", "Build the app", false).option("--seed", "Seed the database", false).option("--refresh", "Force fresh discovery scan", false).option("--json", "Output as JSON", false).action((path, opts) => {
98978
99269
  try {
98979
- const repoPath = resolve4(path ?? process.cwd());
99270
+ const repoPath = resolve5(path ?? process.cwd());
98980
99271
  const snapshot = discoverRepo({ repoPath, refresh: opts.refresh });
98981
99272
  const steps = [];
98982
99273
  if (opts.all) {
@@ -99054,7 +99345,7 @@ repoCmd.command("run [path]").description("Run discovered Playwright tests nativ
99054
99345
  return acc;
99055
99346
  }, []).option("--timeout <ms>", "Timeout per spec file", "300000").option("--url <url>", "Dev server URL").option("--project <id>", "Project ID for result storage").option("--label <text>", "Run label").option("--json", "Output as JSON", false).action(async (path, opts) => {
99056
99347
  try {
99057
- const repoPath = resolve4(path ?? process.cwd());
99348
+ const repoPath = resolve5(path ?? process.cwd());
99058
99349
  const snapshot = discoverRepo({
99059
99350
  repoPath,
99060
99351
  refresh: opts.refresh,
@@ -99120,7 +99411,7 @@ repoCmd.command("run [path]").description("Run discovered Playwright tests nativ
99120
99411
  repoCmd.command("cache [path]").description("Manage discovery cache").option("--clear", "Clear discovery cache", false).option("--status", "Show cache status", false).action((path, opts) => {
99121
99412
  try {
99122
99413
  if (opts.clear) {
99123
- const repoPath2 = path ? resolve4(path) : undefined;
99414
+ const repoPath2 = path ? resolve5(path) : undefined;
99124
99415
  clearDiscoveryCache(repoPath2);
99125
99416
  if (repoPath2) {
99126
99417
  log(chalk6.green("Discovery cache cleared for this repo."));
@@ -99130,7 +99421,7 @@ repoCmd.command("cache [path]").description("Manage discovery cache").option("--
99130
99421
  return;
99131
99422
  }
99132
99423
  if (opts.status) {
99133
- const repoPath2 = resolve4(path ?? process.cwd());
99424
+ const repoPath2 = resolve5(path ?? process.cwd());
99134
99425
  const info2 = getDiscoveryCacheInfo(repoPath2);
99135
99426
  if (!info2) {
99136
99427
  log(chalk6.dim("No discovery cache for this repo."));
@@ -99144,7 +99435,7 @@ repoCmd.command("cache [path]").description("Manage discovery cache").option("--
99144
99435
  log("");
99145
99436
  return;
99146
99437
  }
99147
- const repoPath = resolve4(path ?? process.cwd());
99438
+ const repoPath = resolve5(path ?? process.cwd());
99148
99439
  const info = getDiscoveryCacheInfo(repoPath);
99149
99440
  if (!info) {
99150
99441
  log(chalk6.dim("No discovery cache. Run 'testers repo discover' to create one."));
@@ -99233,8 +99524,8 @@ sessionCmd.command("show <id>").description("Show details of a recorded session"
99233
99524
  });
99234
99525
  sessionCmd.command("import <file>").description("Import a session JSON file exported from the Chrome extension").action(async (file2) => {
99235
99526
  try {
99236
- const { readFileSync: readFileSync11 } = await import("fs");
99237
- const raw = readFileSync11(file2, "utf-8");
99527
+ const { readFileSync: readFileSync12 } = await import("fs");
99528
+ const raw = readFileSync12(file2, "utf-8");
99238
99529
  const data = JSON.parse(raw);
99239
99530
  const items = Array.isArray(data) ? data : [data];
99240
99531
  const { createSession: createSession2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
@@ -99474,7 +99765,7 @@ program2.command("daemon").description("Start the scheduler daemon").option("--i
99474
99765
  } catch (err) {
99475
99766
  logError(chalk6.red(`Daemon error: ${err instanceof Error ? err.message : String(err)}`));
99476
99767
  }
99477
- await new Promise((resolve5) => setTimeout(resolve5, intervalMs));
99768
+ await new Promise((resolve6) => setTimeout(resolve6, intervalMs));
99478
99769
  }
99479
99770
  };
99480
99771
  process.on("SIGINT", () => {
@@ -99503,9 +99794,9 @@ program2.command("ci [provider]").description("Print or write a CI workflow (def
99503
99794
  }
99504
99795
  const workflow = generateGitHubActionsWorkflow();
99505
99796
  if (opts.output) {
99506
- const outPath = resolve4(opts.output);
99797
+ const outPath = resolve5(opts.output);
99507
99798
  const outDir = outPath.replace(/\/[^/]*$/, "");
99508
- if (outDir && !existsSync18(outDir)) {
99799
+ if (outDir && !existsSync19(outDir)) {
99509
99800
  mkdirSync15(outDir, { recursive: true });
99510
99801
  }
99511
99802
  writeFileSync7(outPath, workflow, "utf-8");
@@ -99539,11 +99830,11 @@ program2.command("init").description("Initialize a new testing project").option(
99539
99830
  log(` ${chalk6.dim(s2.shortId)} ${s2.name} ${chalk6.dim(`[${s2.tags.join(", ")}]`)}`);
99540
99831
  }
99541
99832
  if (opts.ci === "github") {
99542
- const workflowDir = join20(process.cwd(), ".github", "workflows");
99543
- if (!existsSync18(workflowDir)) {
99833
+ const workflowDir = join21(process.cwd(), ".github", "workflows");
99834
+ if (!existsSync19(workflowDir)) {
99544
99835
  mkdirSync15(workflowDir, { recursive: true });
99545
99836
  }
99546
- const workflowPath = join20(workflowDir, "testers.yml");
99837
+ const workflowPath = join21(workflowDir, "testers.yml");
99547
99838
  writeFileSync7(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
99548
99839
  log(` CI: ${chalk6.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
99549
99840
  } else if (opts.ci) {
@@ -99553,7 +99844,7 @@ program2.command("init").description("Initialize a new testing project").option(
99553
99844
  if (opts.yes)
99554
99845
  return;
99555
99846
  const rl2 = createInterface({ input: process.stdin, output: process.stdout });
99556
- const ask = (q2) => new Promise((resolve5) => rl2.question(q2, resolve5));
99847
+ const ask = (q2) => new Promise((resolve6) => rl2.question(q2, resolve6));
99557
99848
  try {
99558
99849
  const envAnswer = await ask(" Would you like to configure environments? [y/N] ");
99559
99850
  if (envAnswer.trim().toLowerCase() === "y") {
@@ -99737,14 +100028,14 @@ program2.command("quick-qa <url>").alias("quick-check").description("Run a fast
99737
100028
  wcagLevel
99738
100029
  });
99739
100030
  if (opts.output) {
99740
- writeFileSync7(resolve4(opts.output), JSON.stringify(result, null, 2));
100031
+ writeFileSync7(resolve5(opts.output), JSON.stringify(result, null, 2));
99741
100032
  }
99742
100033
  if (opts.json) {
99743
100034
  log(JSON.stringify(result, null, 2));
99744
100035
  } else {
99745
100036
  log(formatQuickQaReport(result));
99746
100037
  if (opts.output)
99747
- log(chalk6.dim(`Wrote JSON results to ${resolve4(opts.output)}`));
100038
+ log(chalk6.dim(`Wrote JSON results to ${resolve5(opts.output)}`));
99748
100039
  }
99749
100040
  process.exit(getQuickQaExitCode(result));
99750
100041
  } catch (error40) {
@@ -99789,7 +100080,7 @@ program2.command("report [run-id]").description("Generate HTML test report or co
99789
100080
  });
99790
100081
  if (opts.output && opts.output !== "report.html") {
99791
100082
  writeFileSync7(opts.output, content, "utf-8");
99792
- const absPath2 = resolve4(opts.output);
100083
+ const absPath2 = resolve5(opts.output);
99793
100084
  log(chalk6.green(`Compliance report written to ${absPath2}`));
99794
100085
  } else {
99795
100086
  log(content);
@@ -99803,7 +100094,7 @@ program2.command("report [run-id]").description("Generate HTML test report or co
99803
100094
  html = generateHtmlReport(runId);
99804
100095
  }
99805
100096
  writeFileSync7(opts.output, html, "utf-8");
99806
- const absPath = resolve4(opts.output);
100097
+ const absPath = resolve5(opts.output);
99807
100098
  log(chalk6.green(`Report generated: ${absPath}`));
99808
100099
  if (opts.open) {
99809
100100
  const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
@@ -100160,6 +100451,62 @@ Imported ${imported} scenarios from API spec:`));
100160
100451
  process.exit(1);
100161
100452
  }
100162
100453
  });
100454
+ var inventoryCmd = program2.command("inventory").description("Discover source-derived app route/action inventories");
100455
+ inventoryCmd.command("next [root]").description("Discover Next.js app routes and optionally import route coverage scenarios").option("--app-dir <path>", "Next.js app directory relative to root (default: packages/web/app or app)").option("--project <id>", "Project ID").option("--no-pages", "Do not include page.tsx/page.ts routes").option("--no-api", "Do not include route.ts/route.js API routes").option("--limit <n>", "Limit discovered routes").option("--create-scenarios", "Upsert source-derived route coverage scenarios", false).option("--create-workflows", "Upsert grouped workflows by area and route kind", false).option("--workflow-target <target>", "Workflow execution target: local or sandbox", "sandbox").option("--sandbox-provider <provider>", "Sandbox provider for created workflows", "e2b").option("--sandbox-cleanup <mode>", "Sandbox cleanup mode: delete, stop, or keep", "delete").option("--sandbox-sync <strategy>", "Sandbox upload sync strategy: rsync or archive", "rsync").option("--sandbox-env-optional <name>", "Optional sandbox env var; forwards host NAME only when set (repeatable)", (val, acc) => {
100456
+ acc.push(val);
100457
+ return acc;
100458
+ }, []).option("--timeout <ms>", "Workflow timeout in milliseconds").option("--json", "Output as JSON", false).action(async (root, opts) => {
100459
+ try {
100460
+ const { importNextRouteInventory: importNextRouteInventory2 } = await Promise.resolve().then(() => (init_next_route_inventory(), exports_next_route_inventory));
100461
+ const projectId = resolveProject2(opts.project) ?? undefined;
100462
+ const env = parseSandboxEnv(undefined, opts.sandboxEnvOptional);
100463
+ const result = importNextRouteInventory2({
100464
+ rootDir: root ?? process.cwd(),
100465
+ appDir: opts.appDir,
100466
+ projectId,
100467
+ includePages: opts.pages !== false,
100468
+ includeApi: opts.api !== false,
100469
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
100470
+ createScenarios: opts.createScenarios,
100471
+ createWorkflows: opts.createWorkflows,
100472
+ workflowTarget: opts.workflowTarget,
100473
+ workflowProvider: opts.workflowTarget === "sandbox" ? opts.sandboxProvider : undefined,
100474
+ workflowExecution: {
100475
+ target: opts.workflowTarget,
100476
+ provider: opts.workflowTarget === "sandbox" ? opts.sandboxProvider : undefined,
100477
+ sandboxCleanup: opts.sandboxCleanup,
100478
+ sandboxSyncStrategy: opts.sandboxSync,
100479
+ timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
100480
+ env
100481
+ }
100482
+ });
100483
+ if (opts.json) {
100484
+ log(JSON.stringify(result, null, 2));
100485
+ return;
100486
+ }
100487
+ log("");
100488
+ log(chalk6.bold(" Next.js Route Inventory"));
100489
+ log(chalk6.dim(` Root: ${result.inventory.rootDir}`));
100490
+ log(chalk6.dim(` App: ${result.inventory.appDir}`));
100491
+ log("");
100492
+ log(` Routes: ${chalk6.cyan(String(result.inventory.total))} (${result.inventory.pages} pages, ${result.inventory.apiRoutes} API, ${result.inventory.dynamic} dynamic)`);
100493
+ log(` Scenarios: ${chalk6.green(String(result.created))} created, ${chalk6.yellow(String(result.updated))} updated, ${chalk6.dim(String(result.deduped))} deduped`);
100494
+ log(` Workflows: ${result.workflows.length}`);
100495
+ log("");
100496
+ for (const [category, count] of Object.entries(result.inventory.categories).sort()) {
100497
+ log(` ${category.padEnd(18)} ${count}`);
100498
+ }
100499
+ log("");
100500
+ if (!opts.createScenarios)
100501
+ log(chalk6.dim(" Add --create-scenarios to upsert route scenarios."));
100502
+ if (!opts.createWorkflows)
100503
+ log(chalk6.dim(" Add --create-workflows to upsert grouped workflows."));
100504
+ log("");
100505
+ } catch (error40) {
100506
+ logError(chalk6.red(`Error: ${error40 instanceof Error ? error40.message : String(error40)}`));
100507
+ process.exit(1);
100508
+ }
100509
+ });
100163
100510
  program2.command("generate <url>").description("Crawl app and synthesize test scenarios using AI (any provider)").option("--max <n>", "Max scenarios to generate", "10").option("--max-pages <n>", "Max pages to crawl", "10").option("--focus <topic>", "Focus on specific area e.g. 'auth flows', 'checkout'").option("--persona <desc>", "Persona perspective e.g. 'first-time user'").option("--model <model>", "AI model (claude-haiku, gpt-4o-mini, gemini-2.0-flash, etc.)").option("--save", "Persist generated scenarios to DB", false).option("--project <id>", "Project ID").option("--headed", "Run browser in headed mode", false).option("--json", "Output as JSON", false).action(async (url2, opts) => {
100164
100511
  try {
100165
100512
  const { generateScenarios: generateScenarios2 } = await Promise.resolve().then(() => (init_generator(), exports_generator));
@@ -100622,7 +100969,7 @@ program2.command("doctor").description("Check system setup and configuration").a
100622
100969
  log(chalk6.red("\u2717") + " ANTHROPIC_API_KEY is not set (required for AI-powered tests)");
100623
100970
  allPassed = false;
100624
100971
  }
100625
- const dbPath = join20(getTestersDir(), "testers.db");
100972
+ const dbPath = join21(getTestersDir(), "testers.db");
100626
100973
  try {
100627
100974
  const { Database: Database5 } = await import("bun:sqlite");
100628
100975
  const db3 = new Database5(dbPath, { create: true });
@@ -100676,7 +101023,7 @@ program2.command("serve").description("Start the Open Testers web dashboard").op
100676
101023
  try {
100677
101024
  const port = parseInt(opts.port, 10);
100678
101025
  const url2 = `http://localhost:${port}`;
100679
- const serverBin = join20(resolve4(process.execPath, ".."), "..", "dist", "server", "index.js");
101026
+ const serverBin = join21(resolve5(process.execPath, ".."), "..", "dist", "server", "index.js");
100680
101027
  const { join: pathJoin, resolve: pathResolve, dirname: dirname7 } = await import("path");
100681
101028
  const { fileURLToPath: fileURLToPath2 } = await import("url");
100682
101029
  const serverPath = pathJoin(dirname7(fileURLToPath2(import.meta.url)), "..", "server", "index.js");
@@ -101442,7 +101789,7 @@ personaCmd.command("delete <id>").description("Delete a persona").option("-y, --
101442
101789
  }
101443
101790
  if (!opts.yes) {
101444
101791
  process.stdout.write(chalk6.yellow(`Delete persona ${persona.shortId} "${persona.name}"? [y/N] `));
101445
- const answer = await new Promise((resolve5) => {
101792
+ const answer = await new Promise((resolve6) => {
101446
101793
  let buf = "";
101447
101794
  process.stdin.setRawMode?.(true);
101448
101795
  process.stdin.resume();
@@ -101452,7 +101799,7 @@ personaCmd.command("delete <id>").description("Delete a persona").option("-y, --
101452
101799
  process.stdin.pause();
101453
101800
  process.stdout.write(`
101454
101801
  `);
101455
- resolve5(buf);
101802
+ resolve6(buf);
101456
101803
  });
101457
101804
  });
101458
101805
  if (answer !== "y" && answer !== "yes") {
@@ -101613,7 +101960,7 @@ evalCmd.command("rag <url>").description("Run RAG quality evaluation \u2014 fait
101613
101960
  let ragTestCases = [];
101614
101961
  if (opts.docs) {
101615
101962
  try {
101616
- const raw = readFileSync10(opts.docs, "utf-8");
101963
+ const raw = readFileSync11(opts.docs, "utf-8");
101617
101964
  ragTestCases = JSON.parse(raw);
101618
101965
  } catch {
101619
101966
  logError(chalk6.red(`Failed to read docs file: ${opts.docs}`));
@@ -101703,9 +102050,9 @@ Created golden answer check ${chalk6.bold(golden2.shortId)}`));
101703
102050
  }
101704
102051
  const ask = (prompt) => {
101705
102052
  const rl2 = createInterface({ input: process.stdin, output: process.stdout });
101706
- return new Promise((resolve5) => rl2.question(prompt, (ans) => {
102053
+ return new Promise((resolve6) => rl2.question(prompt, (ans) => {
101707
102054
  rl2.close();
101708
- resolve5(ans.trim());
102055
+ resolve6(ans.trim());
101709
102056
  }));
101710
102057
  };
101711
102058
  const question = await ask("Question (what this endpoint should answer): ");
@@ -101866,9 +102213,9 @@ program2.command("run-many <url>").description("Run scenarios \xD7 personas matr
101866
102213
  });
101867
102214
  program2.command("run-script <file>").description("Run a hybrid test script (.ts) that exports an array of HybridScenario objects").option("--url <url>", "Base URL to run against").option("--json", "Output as JSON", false).action(async (file2, opts) => {
101868
102215
  try {
101869
- const { resolve: resolve5 } = await import("path");
102216
+ const { resolve: resolve6 } = await import("path");
101870
102217
  const { runHybridScenario: runHybridScenario2 } = await Promise.resolve().then(() => (init_hybrid_runner(), exports_hybrid_runner));
101871
- const scriptPath = resolve5(process.cwd(), file2);
102218
+ const scriptPath = resolve6(process.cwd(), file2);
101872
102219
  const mod = await import(scriptPath);
101873
102220
  const scenarios = mod.scenarios ?? mod.default ?? [];
101874
102221
  if (!Array.isArray(scenarios) || scenarios.length === 0) {
@@ -0,0 +1,55 @@
1
+ import type { CreateScenarioInput, Scenario, ScenarioPriority, TestingWorkflow, WorkflowExecutionInput } from "../types/index.js";
2
+ export type NextRouteKind = "page" | "api";
3
+ export interface NextRouteInventoryItem {
4
+ kind: NextRouteKind;
5
+ routePath: string;
6
+ file: string;
7
+ category: string;
8
+ groups: string[];
9
+ methods: string[];
10
+ dynamic: boolean;
11
+ requiresAuth: boolean;
12
+ tags: string[];
13
+ priority: ScenarioPriority;
14
+ }
15
+ export interface NextRouteInventory {
16
+ rootDir: string;
17
+ appDir: string;
18
+ total: number;
19
+ pages: number;
20
+ apiRoutes: number;
21
+ dynamic: number;
22
+ categories: Record<string, number>;
23
+ items: NextRouteInventoryItem[];
24
+ }
25
+ export interface ImportNextRouteInventoryOptions {
26
+ rootDir: string;
27
+ appDir?: string;
28
+ projectId?: string;
29
+ includePages?: boolean;
30
+ includeApi?: boolean;
31
+ limit?: number;
32
+ createScenarios?: boolean;
33
+ createWorkflows?: boolean;
34
+ workflowTarget?: "local" | "sandbox";
35
+ workflowProvider?: string;
36
+ workflowExecution?: Partial<WorkflowExecutionInput>;
37
+ }
38
+ export interface ImportNextRouteInventoryResult {
39
+ inventory: NextRouteInventory;
40
+ created: number;
41
+ updated: number;
42
+ deduped: number;
43
+ scenarios: Scenario[];
44
+ workflows: TestingWorkflow[];
45
+ }
46
+ export declare function discoverNextRouteInventory(options: {
47
+ rootDir: string;
48
+ appDir?: string;
49
+ includePages?: boolean;
50
+ includeApi?: boolean;
51
+ limit?: number;
52
+ }): NextRouteInventory;
53
+ export declare function scenarioInputForNextRoute(item: NextRouteInventoryItem, projectId?: string): CreateScenarioInput;
54
+ export declare function importNextRouteInventory(options: ImportNextRouteInventoryOptions): ImportNextRouteInventoryResult;
55
+ //# sourceMappingURL=next-route-inventory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next-route-inventory.d.ts","sourceRoot":"","sources":["../../src/lib/next-route-inventory.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,mBAAmB,EACnB,QAAQ,EACR,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACvB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,KAAK,CAAC;AAE3C,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,aAAa,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,KAAK,EAAE,sBAAsB,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,+BAA+B;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACrC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,8BAA8B;IAC7C,SAAS,EAAE,kBAAkB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,SAAS,EAAE,eAAe,EAAE,CAAC;CAC9B;AAwBD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,kBAAkB,CA4BrB;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,sBAAsB,EAC5B,SAAS,CAAC,EAAE,MAAM,GACjB,mBAAmB,CA0CrB;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,+BAA+B,GACvC,8BAA8B,CAuBhC"}
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.48",
55
+ version: "0.0.49",
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",
@@ -46937,7 +46937,7 @@ import { join as join14 } from "path";
46937
46937
  // package.json
46938
46938
  var package_default = {
46939
46939
  name: "@hasna/testers",
46940
- version: "0.0.48",
46940
+ version: "0.0.49",
46941
46941
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
46942
46942
  type: "module",
46943
46943
  main: "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/testers",
3
- "version": "0.0.48",
3
+ "version": "0.0.49",
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",