@hasna/testers 0.0.48 → 0.0.50
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,511 @@ 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 fixtureStep = item.fixtureParams.length > 0 ? `Bind dynamic fixture values for ${item.fixtureParams.map((name) => `:${name}`).join(", ")} before running route actions.` : undefined;
|
|
59188
|
+
const dynamicStep = item.dynamic ? "Substitute dynamic path parameters with valid fixture values from the target org before opening or calling the route." : undefined;
|
|
59189
|
+
const actionSteps = item.actions.slice(0, 16).map(formatActionStep);
|
|
59190
|
+
const pageSteps = [
|
|
59191
|
+
fixtureStep,
|
|
59192
|
+
dynamicStep,
|
|
59193
|
+
`Open the Next.js ${label} ${item.routePath}.`,
|
|
59194
|
+
"Wait for the route to finish loading and verify it does not show a blank shell, framework error page, or unexpected auth loop.",
|
|
59195
|
+
...actionSteps.length > 0 ? actionSteps : ["Exercise visible primary navigation, tabs, filters, dialogs, forms, and safe buttons on this route."],
|
|
59196
|
+
"Verify the route stays within the expected org/workspace context and does not emit console errors."
|
|
59197
|
+
].filter(Boolean);
|
|
59198
|
+
const apiSteps = [
|
|
59199
|
+
fixtureStep,
|
|
59200
|
+
dynamicStep,
|
|
59201
|
+
`Call the ${methodList} handler(s) for ${item.routePath} using safe fixture data.`,
|
|
59202
|
+
...actionSteps,
|
|
59203
|
+
"Verify expected authentication, authorization, validation, and tenant isolation behavior.",
|
|
59204
|
+
"For mutating methods, use harmless test payloads and confirm the response does not create cross-org side effects.",
|
|
59205
|
+
"Verify response status, JSON shape, and error messages are stable and regression-safe."
|
|
59206
|
+
].filter(Boolean);
|
|
59207
|
+
return {
|
|
59208
|
+
name: `Next ${label}: ${item.routePath}`,
|
|
59209
|
+
description: `Source-discovered ${label} from ${item.file}. Verify route behavior and regressions for ${item.category}.`,
|
|
59210
|
+
steps: item.kind === "page" ? pageSteps : apiSteps,
|
|
59211
|
+
tags: item.tags,
|
|
59212
|
+
priority: item.priority,
|
|
59213
|
+
targetPath: item.routePath,
|
|
59214
|
+
requiresAuth: item.requiresAuth,
|
|
59215
|
+
assertions: item.kind === "page" ? SAFE_PAGE_ASSERTIONS : [],
|
|
59216
|
+
metadata: {
|
|
59217
|
+
source: "next-route-inventory",
|
|
59218
|
+
routeFile: item.file,
|
|
59219
|
+
routeKind: item.kind,
|
|
59220
|
+
category: item.category,
|
|
59221
|
+
methods: item.methods,
|
|
59222
|
+
dynamic: item.dynamic,
|
|
59223
|
+
fixtureParams: item.fixtureParams,
|
|
59224
|
+
actions: item.actions,
|
|
59225
|
+
actionCount: item.actions.length,
|
|
59226
|
+
groups: item.groups
|
|
59227
|
+
},
|
|
59228
|
+
projectId
|
|
59229
|
+
};
|
|
59230
|
+
}
|
|
59231
|
+
function importNextRouteInventory(options) {
|
|
59232
|
+
const inventory = discoverNextRouteInventory(options);
|
|
59233
|
+
let created = 0;
|
|
59234
|
+
let updated = 0;
|
|
59235
|
+
let deduped = 0;
|
|
59236
|
+
const scenarios = [];
|
|
59237
|
+
const workflows = [];
|
|
59238
|
+
if (options.createScenarios) {
|
|
59239
|
+
for (const item of inventory.items) {
|
|
59240
|
+
const result = upsertScenario(scenarioInputForNextRoute(item, options.projectId));
|
|
59241
|
+
scenarios.push(result.scenario);
|
|
59242
|
+
if (result.action === "created")
|
|
59243
|
+
created++;
|
|
59244
|
+
else if (result.action === "updated")
|
|
59245
|
+
updated++;
|
|
59246
|
+
else
|
|
59247
|
+
deduped++;
|
|
59248
|
+
}
|
|
59249
|
+
}
|
|
59250
|
+
if (options.createWorkflows) {
|
|
59251
|
+
workflows.push(...upsertRouteInventoryWorkflows(inventory, options));
|
|
59252
|
+
}
|
|
59253
|
+
return { inventory, created, updated, deduped, scenarios, workflows };
|
|
59254
|
+
}
|
|
59255
|
+
function upsertRouteInventoryWorkflows(inventory, options) {
|
|
59256
|
+
const workflows = [];
|
|
59257
|
+
const categories = Object.keys(inventory.categories).sort();
|
|
59258
|
+
for (const category of categories) {
|
|
59259
|
+
const kinds = new Set(inventory.items.filter((item) => item.category === category).map((item) => item.kind));
|
|
59260
|
+
for (const kind of kinds) {
|
|
59261
|
+
const name = `Next route inventory ${category} ${kind}`;
|
|
59262
|
+
const scenarioTags = ["next-route", `area:${category}`, `route:${kind}`];
|
|
59263
|
+
const execution = {
|
|
59264
|
+
target: options.workflowTarget ?? "sandbox",
|
|
59265
|
+
provider: options.workflowProvider,
|
|
59266
|
+
sandboxCleanup: "delete",
|
|
59267
|
+
sandboxSyncStrategy: "rsync",
|
|
59268
|
+
...options.workflowExecution
|
|
59269
|
+
};
|
|
59270
|
+
const existing = listTestingWorkflows({ projectId: options.projectId, enabled: undefined }).find((workflow) => workflow.name === name);
|
|
59271
|
+
const input = {
|
|
59272
|
+
name,
|
|
59273
|
+
description: `Source-discovered Next.js ${kind} coverage for ${category} routes.`,
|
|
59274
|
+
projectId: options.projectId,
|
|
59275
|
+
scenarioFilter: { tags: scenarioTags },
|
|
59276
|
+
execution
|
|
59277
|
+
};
|
|
59278
|
+
workflows.push(existing ? updateTestingWorkflow(existing.id, input) : createTestingWorkflow(input));
|
|
59279
|
+
}
|
|
59280
|
+
}
|
|
59281
|
+
return workflows;
|
|
59282
|
+
}
|
|
59283
|
+
function resolveAppDir(rootDir, appDir) {
|
|
59284
|
+
const candidates = appDir ? [resolve3(rootDir, appDir)] : [
|
|
59285
|
+
join20(rootDir, "packages", "web", "app"),
|
|
59286
|
+
join20(rootDir, "app"),
|
|
59287
|
+
rootDir
|
|
59288
|
+
];
|
|
59289
|
+
for (const candidate of candidates) {
|
|
59290
|
+
if (existsSync18(candidate) && statSync4(candidate).isDirectory())
|
|
59291
|
+
return candidate;
|
|
59292
|
+
}
|
|
59293
|
+
throw new Error(`Next.js app directory not found under ${rootDir}`);
|
|
59294
|
+
}
|
|
59295
|
+
function walkRouteFiles(appDir) {
|
|
59296
|
+
const files = [];
|
|
59297
|
+
function walk(dir) {
|
|
59298
|
+
for (const entry of readdirSync6(dir)) {
|
|
59299
|
+
if (WALK_EXCLUDES.has(entry))
|
|
59300
|
+
continue;
|
|
59301
|
+
const fullPath = join20(dir, entry);
|
|
59302
|
+
const stat = statSync4(fullPath);
|
|
59303
|
+
if (stat.isDirectory()) {
|
|
59304
|
+
walk(fullPath);
|
|
59305
|
+
} else if (ROUTE_FILE_NAMES.has(entry)) {
|
|
59306
|
+
files.push(fullPath);
|
|
59307
|
+
}
|
|
59308
|
+
}
|
|
59309
|
+
}
|
|
59310
|
+
walk(appDir);
|
|
59311
|
+
return files;
|
|
59312
|
+
}
|
|
59313
|
+
function routeItemFromFile(rootDir, appDir, file) {
|
|
59314
|
+
const fileName = basename3(file);
|
|
59315
|
+
const kind = fileName.startsWith("page.") ? "page" : "api";
|
|
59316
|
+
const relativeFile = relative4(rootDir, file);
|
|
59317
|
+
const appRelative = relative4(appDir, file).split(/[\\/]/);
|
|
59318
|
+
const routeSegments = appRelative.slice(0, -1);
|
|
59319
|
+
const groups = routeSegments.filter((segment) => segment.startsWith("(") && segment.endsWith(")")).map((segment) => segment.slice(1, -1));
|
|
59320
|
+
const pathSegments = routeSegments.filter((segment) => !segment.startsWith("(")).filter((segment) => !segment.startsWith("@")).map(normalizeRouteSegment).filter(Boolean);
|
|
59321
|
+
const routePath = `/${pathSegments.join("/")}`.replace(/\/+/g, "/");
|
|
59322
|
+
const normalizedRoutePath = routePath === "/" ? "/" : routePath.replace(/\/$/, "");
|
|
59323
|
+
const sources = collectRouteSources(rootDir, file);
|
|
59324
|
+
const primarySource = sources[0]?.source ?? readFileSync9(file, "utf8");
|
|
59325
|
+
const methods = kind === "api" ? extractRouteMethods(primarySource) : [];
|
|
59326
|
+
const category = classifyRoute(normalizedRoutePath, groups, relativeFile);
|
|
59327
|
+
const dynamic = routeSegments.some((segment) => segment.includes("["));
|
|
59328
|
+
const fixtureParams = extractFixtureParams(normalizedRoutePath);
|
|
59329
|
+
const actions = kind === "api" ? methods.map((method) => ({
|
|
59330
|
+
kind: "api-method",
|
|
59331
|
+
label: method,
|
|
59332
|
+
target: normalizedRoutePath,
|
|
59333
|
+
sourceFile: relativeFile,
|
|
59334
|
+
destructive: isDestructiveAction(method, normalizedRoutePath),
|
|
59335
|
+
requiresFixture: fixtureParams.length > 0
|
|
59336
|
+
})) : extractPageActions(rootDir, sources, fixtureParams);
|
|
59337
|
+
const requiresAuth = inferRequiresAuth(normalizedRoutePath, groups, kind);
|
|
59338
|
+
return {
|
|
59339
|
+
kind,
|
|
59340
|
+
routePath: normalizedRoutePath,
|
|
59341
|
+
file: relativeFile,
|
|
59342
|
+
category,
|
|
59343
|
+
groups,
|
|
59344
|
+
methods,
|
|
59345
|
+
dynamic,
|
|
59346
|
+
requiresAuth,
|
|
59347
|
+
fixtureParams,
|
|
59348
|
+
actions,
|
|
59349
|
+
tags: tagsForRoute({ kind, routePath: normalizedRoutePath, category, groups, dynamic, requiresAuth }),
|
|
59350
|
+
priority: priorityForRoute(normalizedRoutePath, category, kind)
|
|
59351
|
+
};
|
|
59352
|
+
}
|
|
59353
|
+
function normalizeRouteSegment(segment) {
|
|
59354
|
+
if (segment.startsWith("[[...") && segment.endsWith("]]")) {
|
|
59355
|
+
return `:${segment.slice(5, -2)}*?`;
|
|
59356
|
+
}
|
|
59357
|
+
if (segment.startsWith("[...") && segment.endsWith("]")) {
|
|
59358
|
+
return `:${segment.slice(4, -1)}*`;
|
|
59359
|
+
}
|
|
59360
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
59361
|
+
return `:${segment.slice(1, -1)}`;
|
|
59362
|
+
}
|
|
59363
|
+
return segment;
|
|
59364
|
+
}
|
|
59365
|
+
function extractRouteMethods(source) {
|
|
59366
|
+
const methods = new Set;
|
|
59367
|
+
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;
|
|
59368
|
+
for (const match of source.matchAll(pattern)) {
|
|
59369
|
+
const method = match[1] ?? match[2];
|
|
59370
|
+
if (method)
|
|
59371
|
+
methods.add(method);
|
|
59372
|
+
}
|
|
59373
|
+
return [...methods].sort();
|
|
59374
|
+
}
|
|
59375
|
+
function collectRouteSources(rootDir, entryFile) {
|
|
59376
|
+
const seen = new Set;
|
|
59377
|
+
const sources = [];
|
|
59378
|
+
function visit(file, depth) {
|
|
59379
|
+
if (seen.has(file) || sources.length >= IMPORT_SCAN_LIMIT)
|
|
59380
|
+
return;
|
|
59381
|
+
if (!existsSync18(file) || !statSync4(file).isFile())
|
|
59382
|
+
return;
|
|
59383
|
+
seen.add(file);
|
|
59384
|
+
const source = readFileSync9(file, "utf8");
|
|
59385
|
+
sources.push({ file: relative4(rootDir, file), source });
|
|
59386
|
+
if (depth >= IMPORT_SCAN_DEPTH)
|
|
59387
|
+
return;
|
|
59388
|
+
for (const specifier of localImportSpecifiers(source)) {
|
|
59389
|
+
const resolved = resolveImportFile(file, specifier);
|
|
59390
|
+
if (resolved)
|
|
59391
|
+
visit(resolved, depth + 1);
|
|
59392
|
+
}
|
|
59393
|
+
}
|
|
59394
|
+
visit(entryFile, 0);
|
|
59395
|
+
return sources;
|
|
59396
|
+
}
|
|
59397
|
+
function localImportSpecifiers(source) {
|
|
59398
|
+
const specifiers = [];
|
|
59399
|
+
const pattern = /\bimport\b(?:[\s\S]*?\bfrom\s*)?["'](\.{1,2}\/[^"']+)["']|\bimport\(\s*["'](\.{1,2}\/[^"']+)["']\s*\)/g;
|
|
59400
|
+
for (const match of source.matchAll(pattern)) {
|
|
59401
|
+
const specifier = match[1] ?? match[2];
|
|
59402
|
+
if (specifier)
|
|
59403
|
+
specifiers.push(specifier);
|
|
59404
|
+
}
|
|
59405
|
+
return specifiers;
|
|
59406
|
+
}
|
|
59407
|
+
function resolveImportFile(fromFile, specifier) {
|
|
59408
|
+
const base = resolve3(join20(fromFile, ".."), specifier);
|
|
59409
|
+
const candidates = [
|
|
59410
|
+
base,
|
|
59411
|
+
...SOURCE_EXTENSIONS.map((ext) => `${base}${ext}`),
|
|
59412
|
+
...SOURCE_EXTENSIONS.map((ext) => join20(base, `index${ext}`))
|
|
59413
|
+
];
|
|
59414
|
+
return candidates.find((candidate) => existsSync18(candidate) && statSync4(candidate).isFile()) ?? null;
|
|
59415
|
+
}
|
|
59416
|
+
function extractPageActions(rootDir, sources, fixtureParams) {
|
|
59417
|
+
const actions = [];
|
|
59418
|
+
for (const source of sources) {
|
|
59419
|
+
actions.push(...extractLinkedActions(source, fixtureParams));
|
|
59420
|
+
actions.push(...extractButtonActions(source, fixtureParams));
|
|
59421
|
+
actions.push(...extractFormActions(source, fixtureParams));
|
|
59422
|
+
actions.push(...extractInputActions(source, fixtureParams));
|
|
59423
|
+
}
|
|
59424
|
+
const deduped = new Map;
|
|
59425
|
+
for (const action of actions) {
|
|
59426
|
+
const key = [
|
|
59427
|
+
action.kind,
|
|
59428
|
+
normalizeLabel(action.label),
|
|
59429
|
+
action.target ?? "",
|
|
59430
|
+
action.sourceFile
|
|
59431
|
+
].join("|");
|
|
59432
|
+
if (!deduped.has(key))
|
|
59433
|
+
deduped.set(key, action);
|
|
59434
|
+
}
|
|
59435
|
+
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)) }));
|
|
59436
|
+
}
|
|
59437
|
+
function extractLinkedActions(source, fixtureParams) {
|
|
59438
|
+
const actions = [];
|
|
59439
|
+
const pattern = /<(Link|a)\b([^>]*)>([\s\S]*?)<\/\1>/g;
|
|
59440
|
+
for (const match of source.source.matchAll(pattern)) {
|
|
59441
|
+
const attrs = match[2] ?? "";
|
|
59442
|
+
const body = match[3] ?? "";
|
|
59443
|
+
const href = attributeValue(attrs, "href");
|
|
59444
|
+
const label = firstNonEmpty(attributeValue(attrs, "aria-label"), attributeValue(attrs, "title"), textFromJsx(body), href);
|
|
59445
|
+
if (!label)
|
|
59446
|
+
continue;
|
|
59447
|
+
actions.push({
|
|
59448
|
+
kind: "link",
|
|
59449
|
+
label: clamp(label),
|
|
59450
|
+
target: href ? clamp(href, 180) : undefined,
|
|
59451
|
+
sourceFile: source.file,
|
|
59452
|
+
destructive: isDestructiveAction(label, href ?? ""),
|
|
59453
|
+
requiresFixture: requiresFixture(href ?? "", fixtureParams)
|
|
59454
|
+
});
|
|
59455
|
+
}
|
|
59456
|
+
return actions;
|
|
59457
|
+
}
|
|
59458
|
+
function extractButtonActions(source, fixtureParams) {
|
|
59459
|
+
const actions = [];
|
|
59460
|
+
const pattern = /<(button|Button|IconButton|DropdownMenuItem|CommandItem|SelectItem|TabsTrigger)\b([^>]*?)(?:>([\s\S]*?)<\/\1>|\/>)/g;
|
|
59461
|
+
for (const match of source.source.matchAll(pattern)) {
|
|
59462
|
+
const attrs = match[2] ?? "";
|
|
59463
|
+
const body = match[3] ?? "";
|
|
59464
|
+
const label = firstNonEmpty(attributeValue(attrs, "aria-label"), attributeValue(attrs, "title"), attributeValue(attrs, "data-testid"), textFromJsx(body), attributeValue(attrs, "value"));
|
|
59465
|
+
if (!label)
|
|
59466
|
+
continue;
|
|
59467
|
+
const target = attributeValue(attrs, "href") ?? attributeValue(attrs, "data-testid");
|
|
59468
|
+
actions.push({
|
|
59469
|
+
kind: "button",
|
|
59470
|
+
label: clamp(label),
|
|
59471
|
+
target: target ? clamp(target, 180) : undefined,
|
|
59472
|
+
sourceFile: source.file,
|
|
59473
|
+
destructive: isDestructiveAction(label, attrs),
|
|
59474
|
+
requiresFixture: requiresFixture(`${label} ${target ?? ""}`, fixtureParams)
|
|
59475
|
+
});
|
|
59476
|
+
}
|
|
59477
|
+
return actions;
|
|
59478
|
+
}
|
|
59479
|
+
function extractFormActions(source, fixtureParams) {
|
|
59480
|
+
const actions = [];
|
|
59481
|
+
const pattern = /<form\b([^>]*)>/g;
|
|
59482
|
+
for (const match of source.source.matchAll(pattern)) {
|
|
59483
|
+
const attrs = match[1] ?? "";
|
|
59484
|
+
const label = firstNonEmpty(attributeValue(attrs, "aria-label"), attributeValue(attrs, "name"), attributeValue(attrs, "data-testid"), attributeValue(attrs, "action"), `form in ${source.file}`);
|
|
59485
|
+
const target = attributeValue(attrs, "action");
|
|
59486
|
+
actions.push({
|
|
59487
|
+
kind: "form",
|
|
59488
|
+
label: clamp(label),
|
|
59489
|
+
target: target ? clamp(target, 180) : undefined,
|
|
59490
|
+
sourceFile: source.file,
|
|
59491
|
+
destructive: isDestructiveAction(label, attrs),
|
|
59492
|
+
requiresFixture: requiresFixture(`${label} ${target ?? ""}`, fixtureParams)
|
|
59493
|
+
});
|
|
59494
|
+
}
|
|
59495
|
+
return actions;
|
|
59496
|
+
}
|
|
59497
|
+
function extractInputActions(source, fixtureParams) {
|
|
59498
|
+
const actions = [];
|
|
59499
|
+
const pattern = /<(input|Input|Textarea|Select|Combobox)\b([^>]*?)(?:\/>|>)/g;
|
|
59500
|
+
for (const match of source.source.matchAll(pattern)) {
|
|
59501
|
+
const attrs = match[2] ?? "";
|
|
59502
|
+
const label = firstNonEmpty(attributeValue(attrs, "aria-label"), attributeValue(attrs, "placeholder"), attributeValue(attrs, "name"), attributeValue(attrs, "data-testid"));
|
|
59503
|
+
if (!label)
|
|
59504
|
+
continue;
|
|
59505
|
+
actions.push({
|
|
59506
|
+
kind: "input",
|
|
59507
|
+
label: clamp(label),
|
|
59508
|
+
target: attributeValue(attrs, "name") ?? attributeValue(attrs, "data-testid") ?? undefined,
|
|
59509
|
+
sourceFile: source.file,
|
|
59510
|
+
destructive: false,
|
|
59511
|
+
requiresFixture: requiresFixture(label, fixtureParams)
|
|
59512
|
+
});
|
|
59513
|
+
}
|
|
59514
|
+
return actions;
|
|
59515
|
+
}
|
|
59516
|
+
function attributeValue(attrs, name) {
|
|
59517
|
+
const pattern = new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|\\{\\s*["']([^"']+)["']\\s*\\})`, "i");
|
|
59518
|
+
const match = attrs.match(pattern);
|
|
59519
|
+
const value = match?.[1] ?? match?.[2] ?? match?.[3];
|
|
59520
|
+
return value?.trim() || undefined;
|
|
59521
|
+
}
|
|
59522
|
+
function textFromJsx(value) {
|
|
59523
|
+
const text = value.replace(/<[^>]+>/g, " ").replace(/\{[^}]*\}/g, " ").replace(/\s+/g, " ").trim();
|
|
59524
|
+
return text || undefined;
|
|
59525
|
+
}
|
|
59526
|
+
function firstNonEmpty(...values) {
|
|
59527
|
+
return values.find((value) => value && value.trim())?.trim() ?? "";
|
|
59528
|
+
}
|
|
59529
|
+
function clamp(value, max = 120) {
|
|
59530
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
59531
|
+
return compact.length > max ? `${compact.slice(0, max - 1)}\u2026` : compact;
|
|
59532
|
+
}
|
|
59533
|
+
function normalizeLabel(value) {
|
|
59534
|
+
return value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
59535
|
+
}
|
|
59536
|
+
function extractFixtureParams(routePath) {
|
|
59537
|
+
const params = new Set;
|
|
59538
|
+
for (const match of routePath.matchAll(/:([A-Za-z0-9_]+)(?:\*\??)?/g)) {
|
|
59539
|
+
if (match[1])
|
|
59540
|
+
params.add(match[1]);
|
|
59541
|
+
}
|
|
59542
|
+
return [...params];
|
|
59543
|
+
}
|
|
59544
|
+
function requiresFixture(value, fixtureParams) {
|
|
59545
|
+
if (fixtureParams.length === 0)
|
|
59546
|
+
return false;
|
|
59547
|
+
const normalized = value.toLowerCase();
|
|
59548
|
+
return fixtureParams.some((param) => normalized.includes(param.toLowerCase()) || normalized.includes(`[${param}]`));
|
|
59549
|
+
}
|
|
59550
|
+
function isDestructiveAction(label, context = "") {
|
|
59551
|
+
return /\b(delete|destroy|remove|revoke|refund|void|archive|suspend|pause|disable|cancel|reset|purge|terminate)\b/i.test(`${label} ${context}`);
|
|
59552
|
+
}
|
|
59553
|
+
function formatActionStep(action) {
|
|
59554
|
+
const target = action.target ? ` (${action.target})` : "";
|
|
59555
|
+
const fixture = action.requiresFixture ? " after binding fixture values" : "";
|
|
59556
|
+
const guard = action.destructive ? " without confirming the destructive final action" : "";
|
|
59557
|
+
if (action.kind === "api-method") {
|
|
59558
|
+
return `Exercise API method ${action.label}${target}${fixture}${guard}.`;
|
|
59559
|
+
}
|
|
59560
|
+
return `Exercise ${action.kind} "${action.label}"${target}${fixture}${guard}.`;
|
|
59561
|
+
}
|
|
59562
|
+
function classifyRoute(routePath, groups, file) {
|
|
59563
|
+
const haystack = `${routePath} ${groups.join(" ")} ${file}`.toLowerCase();
|
|
59564
|
+
if (haystack.includes("admin"))
|
|
59565
|
+
return "admin";
|
|
59566
|
+
if (haystack.includes("auth") || routePath.startsWith("/cli/device"))
|
|
59567
|
+
return "auth";
|
|
59568
|
+
if (haystack.includes("ai-runtime") || /\/(chat|sessions|memories|knowledge|learning|copilot|guardrails)\b/.test(routePath))
|
|
59569
|
+
return "ai-runtime";
|
|
59570
|
+
if (haystack.includes("commerce") || /\/(billing|shop|agent-wallet|domains|whois-profiles)\b/.test(routePath))
|
|
59571
|
+
return "commerce";
|
|
59572
|
+
if (haystack.includes("communications") || /\/(telephony|emails)\b/.test(routePath))
|
|
59573
|
+
return "communications";
|
|
59574
|
+
if (haystack.includes("crm") || routePath.includes("/contacts"))
|
|
59575
|
+
return "crm";
|
|
59576
|
+
if (haystack.includes("integrations") || routePath.includes("/connectors"))
|
|
59577
|
+
return "integrations";
|
|
59578
|
+
if (haystack.includes("dashboard") || routePath.includes(":orgSlug"))
|
|
59579
|
+
return "dashboard";
|
|
59580
|
+
if (haystack.includes("public") || haystack.includes("pages"))
|
|
59581
|
+
return "public";
|
|
59582
|
+
if (routePath.startsWith("/api/"))
|
|
59583
|
+
return "api";
|
|
59584
|
+
return "app";
|
|
59585
|
+
}
|
|
59586
|
+
function inferRequiresAuth(routePath, groups, kind) {
|
|
59587
|
+
const haystack = `${routePath} ${groups.join(" ")}`.toLowerCase();
|
|
59588
|
+
if (haystack.includes("auth") || haystack.includes("public") || haystack.includes("webhook"))
|
|
59589
|
+
return false;
|
|
59590
|
+
if (routePath.startsWith("/api/v1/auth/"))
|
|
59591
|
+
return false;
|
|
59592
|
+
if (routePath.startsWith("/api/"))
|
|
59593
|
+
return true;
|
|
59594
|
+
return kind === "page" && (haystack.includes("admin") || haystack.includes("dashboard") || routePath.includes(":orgSlug") || routePath.startsWith("/settings"));
|
|
59595
|
+
}
|
|
59596
|
+
function tagsForRoute(input) {
|
|
59597
|
+
const tags = new Set([
|
|
59598
|
+
"next-route",
|
|
59599
|
+
`route:${input.kind}`,
|
|
59600
|
+
`area:${input.category}`,
|
|
59601
|
+
input.category
|
|
59602
|
+
]);
|
|
59603
|
+
for (const group of input.groups)
|
|
59604
|
+
tags.add(`group:${group}`);
|
|
59605
|
+
if (input.dynamic)
|
|
59606
|
+
tags.add("dynamic-route");
|
|
59607
|
+
if (input.requiresAuth)
|
|
59608
|
+
tags.add("auth-required");
|
|
59609
|
+
if (input.routePath.startsWith("/api/"))
|
|
59610
|
+
tags.add("api");
|
|
59611
|
+
return [...tags];
|
|
59612
|
+
}
|
|
59613
|
+
function priorityForRoute(routePath, category, kind) {
|
|
59614
|
+
if (category === "auth")
|
|
59615
|
+
return "critical";
|
|
59616
|
+
if (category === "commerce" || category === "ai-runtime")
|
|
59617
|
+
return "critical";
|
|
59618
|
+
if (category === "admin" || category === "dashboard")
|
|
59619
|
+
return "high";
|
|
59620
|
+
if (kind === "api")
|
|
59621
|
+
return "high";
|
|
59622
|
+
if (routePath === "/" || category === "public")
|
|
59623
|
+
return "medium";
|
|
59624
|
+
return "medium";
|
|
59625
|
+
}
|
|
59626
|
+
var ROUTE_FILE_NAMES, WALK_EXCLUDES, SAFE_PAGE_ASSERTIONS, IMPORT_SCAN_LIMIT = 40, IMPORT_SCAN_DEPTH = 3, SOURCE_EXTENSIONS;
|
|
59627
|
+
var init_next_route_inventory = __esm(() => {
|
|
59628
|
+
init_scenarios();
|
|
59629
|
+
init_workflows();
|
|
59630
|
+
ROUTE_FILE_NAMES = new Set([
|
|
59631
|
+
"page.tsx",
|
|
59632
|
+
"page.ts",
|
|
59633
|
+
"page.jsx",
|
|
59634
|
+
"page.js",
|
|
59635
|
+
"page.mdx",
|
|
59636
|
+
"route.ts",
|
|
59637
|
+
"route.js"
|
|
59638
|
+
]);
|
|
59639
|
+
WALK_EXCLUDES = new Set([
|
|
59640
|
+
".git",
|
|
59641
|
+
".next",
|
|
59642
|
+
".turbo",
|
|
59643
|
+
"node_modules",
|
|
59644
|
+
"dist",
|
|
59645
|
+
"build",
|
|
59646
|
+
"coverage"
|
|
59647
|
+
]);
|
|
59648
|
+
SAFE_PAGE_ASSERTIONS = [{ type: "no_console_errors" }];
|
|
59649
|
+
SOURCE_EXTENSIONS = [
|
|
59650
|
+
".tsx",
|
|
59651
|
+
".ts",
|
|
59652
|
+
".jsx",
|
|
59653
|
+
".js",
|
|
59654
|
+
".mdx"
|
|
59655
|
+
];
|
|
59656
|
+
});
|
|
59657
|
+
|
|
59153
59658
|
// src/lib/generator.ts
|
|
59154
59659
|
var exports_generator = {};
|
|
59155
59660
|
__export(exports_generator, {
|
|
@@ -59448,7 +59953,7 @@ async function recordSession(url, options) {
|
|
|
59448
59953
|
await Promise.race([
|
|
59449
59954
|
page.waitForEvent("close").catch(() => {}),
|
|
59450
59955
|
context.waitForEvent("close").catch(() => {}),
|
|
59451
|
-
new Promise((
|
|
59956
|
+
new Promise((resolve4) => setTimeout(resolve4, timeout))
|
|
59452
59957
|
]);
|
|
59453
59958
|
clearInterval(pollInterval);
|
|
59454
59959
|
try {
|
|
@@ -77016,7 +77521,7 @@ function createProviderToolFactoryWithOutputSchema({
|
|
|
77016
77521
|
supportsDeferredResults
|
|
77017
77522
|
});
|
|
77018
77523
|
}
|
|
77019
|
-
async function
|
|
77524
|
+
async function resolve4(value) {
|
|
77020
77525
|
if (typeof value === "function") {
|
|
77021
77526
|
value = value();
|
|
77022
77527
|
}
|
|
@@ -78703,7 +79208,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
78703
79208
|
try {
|
|
78704
79209
|
const { value } = await getFromApi({
|
|
78705
79210
|
url: `${this.config.baseURL}/config`,
|
|
78706
|
-
headers: await
|
|
79211
|
+
headers: await resolve4(this.config.headers()),
|
|
78707
79212
|
successfulResponseHandler: createJsonResponseHandler(gatewayAvailableModelsResponseSchema),
|
|
78708
79213
|
failedResponseHandler: createJsonErrorResponseHandler({
|
|
78709
79214
|
errorSchema: exports_external2.any(),
|
|
@@ -78721,7 +79226,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
78721
79226
|
const baseUrl = new URL(this.config.baseURL);
|
|
78722
79227
|
const { value } = await getFromApi({
|
|
78723
79228
|
url: `${baseUrl.origin}/v1/credits`,
|
|
78724
|
-
headers: await
|
|
79229
|
+
headers: await resolve4(this.config.headers()),
|
|
78725
79230
|
successfulResponseHandler: createJsonResponseHandler(gatewayCreditsResponseSchema),
|
|
78726
79231
|
failedResponseHandler: createJsonErrorResponseHandler({
|
|
78727
79232
|
errorSchema: exports_external2.any(),
|
|
@@ -78767,7 +79272,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
78767
79272
|
}
|
|
78768
79273
|
const { value } = await getFromApi({
|
|
78769
79274
|
url: `${baseUrl.origin}/v1/report?${searchParams.toString()}`,
|
|
78770
|
-
headers: await
|
|
79275
|
+
headers: await resolve4(this.config.headers()),
|
|
78771
79276
|
successfulResponseHandler: createJsonResponseHandler(gatewaySpendReportResponseSchema),
|
|
78772
79277
|
failedResponseHandler: createJsonErrorResponseHandler({
|
|
78773
79278
|
errorSchema: exports_external2.any(),
|
|
@@ -78789,7 +79294,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
78789
79294
|
const baseUrl = new URL(this.config.baseURL);
|
|
78790
79295
|
const { value } = await getFromApi({
|
|
78791
79296
|
url: `${baseUrl.origin}/v1/generation?id=${encodeURIComponent(params.id)}`,
|
|
78792
|
-
headers: await
|
|
79297
|
+
headers: await resolve4(this.config.headers()),
|
|
78793
79298
|
successfulResponseHandler: createJsonResponseHandler(gatewayGenerationInfoResponseSchema),
|
|
78794
79299
|
failedResponseHandler: createJsonErrorResponseHandler({
|
|
78795
79300
|
errorSchema: exports_external2.any(),
|
|
@@ -78822,7 +79327,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
78822
79327
|
async doGenerate(options) {
|
|
78823
79328
|
const { args, warnings } = await this.getArgs(options);
|
|
78824
79329
|
const { abortSignal } = options;
|
|
78825
|
-
const resolvedHeaders = await
|
|
79330
|
+
const resolvedHeaders = await resolve4(this.config.headers());
|
|
78826
79331
|
try {
|
|
78827
79332
|
const {
|
|
78828
79333
|
responseHeaders,
|
|
@@ -78830,7 +79335,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
78830
79335
|
rawValue: rawResponse
|
|
78831
79336
|
} = await postJsonToApi({
|
|
78832
79337
|
url: this.getUrl(),
|
|
78833
|
-
headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, false), await
|
|
79338
|
+
headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, false), await resolve4(this.config.o11yHeaders)),
|
|
78834
79339
|
body: args,
|
|
78835
79340
|
successfulResponseHandler: createJsonResponseHandler(exports_external2.any()),
|
|
78836
79341
|
failedResponseHandler: createJsonErrorResponseHandler({
|
|
@@ -78853,11 +79358,11 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
78853
79358
|
async doStream(options) {
|
|
78854
79359
|
const { args, warnings } = await this.getArgs(options);
|
|
78855
79360
|
const { abortSignal } = options;
|
|
78856
|
-
const resolvedHeaders = await
|
|
79361
|
+
const resolvedHeaders = await resolve4(this.config.headers());
|
|
78857
79362
|
try {
|
|
78858
79363
|
const { value: response, responseHeaders } = await postJsonToApi({
|
|
78859
79364
|
url: this.getUrl(),
|
|
78860
|
-
headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, true), await
|
|
79365
|
+
headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, true), await resolve4(this.config.o11yHeaders)),
|
|
78861
79366
|
body: args,
|
|
78862
79367
|
successfulResponseHandler: createEventSourceResponseHandler(exports_external2.any()),
|
|
78863
79368
|
failedResponseHandler: createJsonErrorResponseHandler({
|
|
@@ -78942,7 +79447,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
78942
79447
|
providerOptions
|
|
78943
79448
|
}) {
|
|
78944
79449
|
var _a92;
|
|
78945
|
-
const resolvedHeaders = await
|
|
79450
|
+
const resolvedHeaders = await resolve4(this.config.headers());
|
|
78946
79451
|
try {
|
|
78947
79452
|
const {
|
|
78948
79453
|
responseHeaders,
|
|
@@ -78950,7 +79455,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
78950
79455
|
rawValue
|
|
78951
79456
|
} = await postJsonToApi({
|
|
78952
79457
|
url: this.getUrl(),
|
|
78953
|
-
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await
|
|
79458
|
+
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve4(this.config.o11yHeaders)),
|
|
78954
79459
|
body: {
|
|
78955
79460
|
values,
|
|
78956
79461
|
...providerOptions ? { providerOptions } : {}
|
|
@@ -79006,7 +79511,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
79006
79511
|
abortSignal
|
|
79007
79512
|
}) {
|
|
79008
79513
|
var _a92, _b92, _c2, _d2;
|
|
79009
|
-
const resolvedHeaders = await
|
|
79514
|
+
const resolvedHeaders = await resolve4(this.config.headers());
|
|
79010
79515
|
try {
|
|
79011
79516
|
const {
|
|
79012
79517
|
responseHeaders,
|
|
@@ -79014,7 +79519,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
79014
79519
|
rawValue
|
|
79015
79520
|
} = await postJsonToApi({
|
|
79016
79521
|
url: this.getUrl(),
|
|
79017
|
-
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await
|
|
79522
|
+
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve4(this.config.o11yHeaders)),
|
|
79018
79523
|
body: {
|
|
79019
79524
|
prompt,
|
|
79020
79525
|
n: n2,
|
|
@@ -79089,11 +79594,11 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
79089
79594
|
abortSignal
|
|
79090
79595
|
}) {
|
|
79091
79596
|
var _a92;
|
|
79092
|
-
const resolvedHeaders = await
|
|
79597
|
+
const resolvedHeaders = await resolve4(this.config.headers());
|
|
79093
79598
|
try {
|
|
79094
79599
|
const { responseHeaders, value: responseBody } = await postJsonToApi({
|
|
79095
79600
|
url: this.getUrl(),
|
|
79096
|
-
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await
|
|
79601
|
+
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve4(this.config.o11yHeaders), { accept: "text/event-stream" }),
|
|
79097
79602
|
body: {
|
|
79098
79603
|
prompt,
|
|
79099
79604
|
n: n2,
|
|
@@ -79216,7 +79721,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
79216
79721
|
abortSignal,
|
|
79217
79722
|
providerOptions
|
|
79218
79723
|
}) {
|
|
79219
|
-
const resolvedHeaders = await
|
|
79724
|
+
const resolvedHeaders = await resolve4(this.config.headers());
|
|
79220
79725
|
try {
|
|
79221
79726
|
const {
|
|
79222
79727
|
responseHeaders,
|
|
@@ -79224,7 +79729,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
79224
79729
|
rawValue
|
|
79225
79730
|
} = await postJsonToApi({
|
|
79226
79731
|
url: this.getUrl(),
|
|
79227
|
-
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await
|
|
79732
|
+
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve4(this.config.o11yHeaders)),
|
|
79228
79733
|
body: {
|
|
79229
79734
|
documents,
|
|
79230
79735
|
query,
|
|
@@ -88642,7 +89147,7 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
|
|
|
88642
89147
|
const schema = asSchema(inputSchema);
|
|
88643
89148
|
return {
|
|
88644
89149
|
name: "object",
|
|
88645
|
-
responseFormat:
|
|
89150
|
+
responseFormat: resolve4(schema.jsonSchema).then((jsonSchema2) => ({
|
|
88646
89151
|
type: "json",
|
|
88647
89152
|
schema: jsonSchema2,
|
|
88648
89153
|
...name21 != null && { name: name21 },
|
|
@@ -88703,7 +89208,7 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
|
|
|
88703
89208
|
const elementSchema = asSchema(inputElementSchema);
|
|
88704
89209
|
return {
|
|
88705
89210
|
name: "array",
|
|
88706
|
-
responseFormat:
|
|
89211
|
+
responseFormat: resolve4(elementSchema.jsonSchema).then((jsonSchema2) => {
|
|
88707
89212
|
const { $schema, ...itemSchema } = jsonSchema2;
|
|
88708
89213
|
return {
|
|
88709
89214
|
type: "json",
|
|
@@ -91600,9 +92105,9 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
|
|
|
91600
92105
|
...options
|
|
91601
92106
|
}) {
|
|
91602
92107
|
var _a21, _b16, _c2, _d2, _e2;
|
|
91603
|
-
const resolvedBody = await
|
|
91604
|
-
const resolvedHeaders = await
|
|
91605
|
-
const resolvedCredentials = await
|
|
92108
|
+
const resolvedBody = await resolve4(this.body);
|
|
92109
|
+
const resolvedHeaders = await resolve4(this.headers);
|
|
92110
|
+
const resolvedCredentials = await resolve4(this.credentials);
|
|
91606
92111
|
const baseHeaders = {
|
|
91607
92112
|
...normalizeHeaders(resolvedHeaders),
|
|
91608
92113
|
...normalizeHeaders(options.headers)
|
|
@@ -91650,9 +92155,9 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
|
|
|
91650
92155
|
}
|
|
91651
92156
|
async reconnectToStream(options) {
|
|
91652
92157
|
var _a21, _b16, _c2, _d2, _e2;
|
|
91653
|
-
const resolvedBody = await
|
|
91654
|
-
const resolvedHeaders = await
|
|
91655
|
-
const resolvedCredentials = await
|
|
92158
|
+
const resolvedBody = await resolve4(this.body);
|
|
92159
|
+
const resolvedHeaders = await resolve4(this.headers);
|
|
92160
|
+
const resolvedCredentials = await resolve4(this.credentials);
|
|
91656
92161
|
const baseHeaders = {
|
|
91657
92162
|
...normalizeHeaders(resolvedHeaders),
|
|
91658
92163
|
...normalizeHeaders(options.headers)
|
|
@@ -93873,7 +94378,7 @@ __export(exports_session_converter, {
|
|
|
93873
94378
|
convertSessionToScenario: () => convertSessionToScenario,
|
|
93874
94379
|
convertSessionFile: () => convertSessionFile
|
|
93875
94380
|
});
|
|
93876
|
-
import { readFileSync as
|
|
94381
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
93877
94382
|
import { extname } from "path";
|
|
93878
94383
|
function parseRrwebSession(events) {
|
|
93879
94384
|
const result = [];
|
|
@@ -94058,7 +94563,7 @@ ${condensed}`;
|
|
|
94058
94563
|
};
|
|
94059
94564
|
}
|
|
94060
94565
|
async function convertSessionFile(filePath, format, options) {
|
|
94061
|
-
const raw =
|
|
94566
|
+
const raw = readFileSync10(filePath, "utf-8");
|
|
94062
94567
|
let parsed;
|
|
94063
94568
|
try {
|
|
94064
94569
|
parsed = JSON.parse(raw);
|
|
@@ -94092,7 +94597,7 @@ function detectSessionFormat(filePath) {
|
|
|
94092
94597
|
if (ext === ".har")
|
|
94093
94598
|
return "har";
|
|
94094
94599
|
try {
|
|
94095
|
-
const content =
|
|
94600
|
+
const content = readFileSync10(filePath, "utf-8").trim();
|
|
94096
94601
|
const parsed = JSON.parse(content);
|
|
94097
94602
|
if (Array.isArray(parsed) && parsed[0]?.type !== undefined && typeof parsed[0]?.timestamp === "number") {
|
|
94098
94603
|
return "rrweb";
|
|
@@ -94536,7 +95041,7 @@ import chalk6 from "chalk";
|
|
|
94536
95041
|
// package.json
|
|
94537
95042
|
var package_default = {
|
|
94538
95043
|
name: "@hasna/testers",
|
|
94539
|
-
version: "0.0.
|
|
95044
|
+
version: "0.0.50",
|
|
94540
95045
|
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
94541
95046
|
type: "module",
|
|
94542
95047
|
main: "dist/index.js",
|
|
@@ -94635,9 +95140,9 @@ init_todos_connector();
|
|
|
94635
95140
|
init_browser();
|
|
94636
95141
|
import { render, Box, Text, useInput, useApp } from "ink";
|
|
94637
95142
|
import React, { useState } from "react";
|
|
94638
|
-
import { readFileSync as
|
|
95143
|
+
import { readFileSync as readFileSync11, readdirSync as readdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
94639
95144
|
import { createInterface } from "readline";
|
|
94640
|
-
import { join as
|
|
95145
|
+
import { join as join21, resolve as resolve5 } from "path";
|
|
94641
95146
|
|
|
94642
95147
|
// src/lib/init.ts
|
|
94643
95148
|
init_paths();
|
|
@@ -96723,7 +97228,7 @@ init_ci();
|
|
|
96723
97228
|
init_assertions();
|
|
96724
97229
|
init_paths();
|
|
96725
97230
|
init_sessions();
|
|
96726
|
-
import { existsSync as
|
|
97231
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync15 } from "fs";
|
|
96727
97232
|
|
|
96728
97233
|
// src/lib/repo-discovery.ts
|
|
96729
97234
|
init_paths();
|
|
@@ -97742,18 +98247,18 @@ program2.command("prod-debug <target>").description("Create a safe production de
|
|
|
97742
98247
|
}, config2.prodDebug);
|
|
97743
98248
|
const output = opts.json ? JSON.stringify(plan, null, 2) : formatProdDebugPlan(plan);
|
|
97744
98249
|
if (opts.output) {
|
|
97745
|
-
writeFileSync7(
|
|
98250
|
+
writeFileSync7(resolve5(opts.output), output + `
|
|
97746
98251
|
`);
|
|
97747
98252
|
} else {
|
|
97748
98253
|
log(output);
|
|
97749
98254
|
}
|
|
97750
98255
|
});
|
|
97751
98256
|
var CONFIG_DIR5 = getTestersDir();
|
|
97752
|
-
var CONFIG_PATH4 =
|
|
98257
|
+
var CONFIG_PATH4 = join21(CONFIG_DIR5, "config.json");
|
|
97753
98258
|
function getActiveProject() {
|
|
97754
98259
|
try {
|
|
97755
|
-
if (
|
|
97756
|
-
const raw = JSON.parse(
|
|
98260
|
+
if (existsSync19(CONFIG_PATH4)) {
|
|
98261
|
+
const raw = JSON.parse(readFileSync11(CONFIG_PATH4, "utf-8"));
|
|
97757
98262
|
return raw.activeProject ?? undefined;
|
|
97758
98263
|
}
|
|
97759
98264
|
} catch {}
|
|
@@ -97960,7 +98465,7 @@ program2.command("delete <id>").description("Delete a scenario").option("-y, --y
|
|
|
97960
98465
|
}
|
|
97961
98466
|
if (!opts.yes) {
|
|
97962
98467
|
process.stdout.write(chalk6.yellow(`Delete scenario ${scenario.shortId} "${scenario.name}"? [y/N] `));
|
|
97963
|
-
const answer = await new Promise((
|
|
98468
|
+
const answer = await new Promise((resolve6) => {
|
|
97964
98469
|
let buf = "";
|
|
97965
98470
|
process.stdin.setRawMode?.(true);
|
|
97966
98471
|
process.stdin.resume();
|
|
@@ -97970,7 +98475,7 @@ program2.command("delete <id>").description("Delete a scenario").option("-y, --y
|
|
|
97970
98475
|
process.stdin.pause();
|
|
97971
98476
|
process.stdout.write(`
|
|
97972
98477
|
`);
|
|
97973
|
-
|
|
98478
|
+
resolve6(buf);
|
|
97974
98479
|
});
|
|
97975
98480
|
});
|
|
97976
98481
|
if (answer !== "y" && answer !== "yes") {
|
|
@@ -97999,7 +98504,7 @@ program2.command("remove <id>").alias("uninstall").description("Remove a scenari
|
|
|
97999
98504
|
}
|
|
98000
98505
|
if (!opts.yes) {
|
|
98001
98506
|
process.stdout.write(chalk6.yellow(`Remove scenario ${scenario.shortId} "${scenario.name}"? [y/N] `));
|
|
98002
|
-
const answer = await new Promise((
|
|
98507
|
+
const answer = await new Promise((resolve6) => {
|
|
98003
98508
|
let buf = "";
|
|
98004
98509
|
process.stdin.setRawMode?.(true);
|
|
98005
98510
|
process.stdin.resume();
|
|
@@ -98009,7 +98514,7 @@ program2.command("remove <id>").alias("uninstall").description("Remove a scenari
|
|
|
98009
98514
|
process.stdin.pause();
|
|
98010
98515
|
process.stdout.write(`
|
|
98011
98516
|
`);
|
|
98012
|
-
|
|
98517
|
+
resolve6(buf);
|
|
98013
98518
|
});
|
|
98014
98519
|
});
|
|
98015
98520
|
if (answer !== "y" && answer !== "yes") {
|
|
@@ -98212,7 +98717,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
98212
98717
|
`);
|
|
98213
98718
|
}
|
|
98214
98719
|
};
|
|
98215
|
-
await new Promise((
|
|
98720
|
+
await new Promise((resolve6) => {
|
|
98216
98721
|
const poll = setInterval(() => {
|
|
98217
98722
|
const run3 = getRun(runId);
|
|
98218
98723
|
if (!run3)
|
|
@@ -98220,7 +98725,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
98220
98725
|
renderTable();
|
|
98221
98726
|
if (DONE_STATUSES.has(run3.status)) {
|
|
98222
98727
|
clearInterval(poll);
|
|
98223
|
-
|
|
98728
|
+
resolve6();
|
|
98224
98729
|
}
|
|
98225
98730
|
}, POLL_INTERVAL);
|
|
98226
98731
|
});
|
|
@@ -98615,15 +99120,15 @@ program2.command("screenshots <id>").description("List screenshots for a run or
|
|
|
98615
99120
|
});
|
|
98616
99121
|
program2.command("import <dir>").description("Import markdown test files as scenarios").action((dir) => {
|
|
98617
99122
|
try {
|
|
98618
|
-
const absDir =
|
|
98619
|
-
const files =
|
|
99123
|
+
const absDir = resolve5(dir);
|
|
99124
|
+
const files = readdirSync7(absDir).filter((f2) => f2.endsWith(".md"));
|
|
98620
99125
|
if (files.length === 0) {
|
|
98621
99126
|
log(chalk6.dim("No .md files found in directory."));
|
|
98622
99127
|
return;
|
|
98623
99128
|
}
|
|
98624
99129
|
let imported = 0;
|
|
98625
99130
|
for (const file2 of files) {
|
|
98626
|
-
const content =
|
|
99131
|
+
const content = readFileSync11(join21(absDir, file2), "utf-8");
|
|
98627
99132
|
const lines = content.split(`
|
|
98628
99133
|
`);
|
|
98629
99134
|
let name21 = file2.replace(/\.md$/, "");
|
|
@@ -98679,11 +99184,11 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
|
|
|
98679
99184
|
const outputPath = opts.output ?? "testers-export.json";
|
|
98680
99185
|
const data = JSON.stringify(scenarios, null, 2);
|
|
98681
99186
|
writeFileSync7(outputPath, data, "utf-8");
|
|
98682
|
-
log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${
|
|
99187
|
+
log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${resolve5(outputPath)}`));
|
|
98683
99188
|
return;
|
|
98684
99189
|
}
|
|
98685
99190
|
const outputDir = opts.output ?? ".";
|
|
98686
|
-
if (!
|
|
99191
|
+
if (!existsSync19(outputDir)) {
|
|
98687
99192
|
mkdirSync15(outputDir, { recursive: true });
|
|
98688
99193
|
}
|
|
98689
99194
|
for (const s2 of scenarios) {
|
|
@@ -98711,13 +99216,13 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
|
|
|
98711
99216
|
lines.push("");
|
|
98712
99217
|
}
|
|
98713
99218
|
const safeFilename = s2.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
|
|
98714
|
-
const filePath =
|
|
99219
|
+
const filePath = join21(outputDir, `${s2.shortId}-${safeFilename}.md`);
|
|
98715
99220
|
writeFileSync7(filePath, lines.join(`
|
|
98716
99221
|
`), "utf-8");
|
|
98717
99222
|
log(chalk6.dim(` ${s2.shortId}: ${s2.name} \u2192 ${filePath}`));
|
|
98718
99223
|
}
|
|
98719
99224
|
log(chalk6.green(`
|
|
98720
|
-
Exported ${scenarios.length} scenario(s) as markdown to ${
|
|
99225
|
+
Exported ${scenarios.length} scenario(s) as markdown to ${resolve5(outputDir)}`));
|
|
98721
99226
|
} catch (error40) {
|
|
98722
99227
|
logError(chalk6.red(`Error: ${error40 instanceof Error ? error40.message : String(error40)}`));
|
|
98723
99228
|
process.exit(1);
|
|
@@ -98736,7 +99241,7 @@ program2.command("status").description("Show database and auth status").action((
|
|
|
98736
99241
|
try {
|
|
98737
99242
|
const config2 = loadConfig();
|
|
98738
99243
|
const hasApiKey = !!config2.anthropicApiKey || !!process.env["ANTHROPIC_API_KEY"];
|
|
98739
|
-
const dbPath =
|
|
99244
|
+
const dbPath = join21(getTestersDir(), "testers.db");
|
|
98740
99245
|
log("");
|
|
98741
99246
|
log(chalk6.bold(" Open Testers Status"));
|
|
98742
99247
|
log("");
|
|
@@ -98886,13 +99391,13 @@ projectCmd.command("export-open <id>").description("Register a testers project i
|
|
|
98886
99391
|
projectCmd.command("use <name>").description("Set active project (find or create)").option("--json", "Output as JSON", false).action((name21, opts) => {
|
|
98887
99392
|
try {
|
|
98888
99393
|
const project = ensureProject(name21, process.cwd());
|
|
98889
|
-
if (!
|
|
99394
|
+
if (!existsSync19(CONFIG_DIR5)) {
|
|
98890
99395
|
mkdirSync15(CONFIG_DIR5, { recursive: true });
|
|
98891
99396
|
}
|
|
98892
99397
|
let config2 = {};
|
|
98893
|
-
if (
|
|
99398
|
+
if (existsSync19(CONFIG_PATH4)) {
|
|
98894
99399
|
try {
|
|
98895
|
-
config2 = JSON.parse(
|
|
99400
|
+
config2 = JSON.parse(readFileSync11(CONFIG_PATH4, "utf-8"));
|
|
98896
99401
|
} catch {}
|
|
98897
99402
|
}
|
|
98898
99403
|
config2.activeProject = project.id;
|
|
@@ -98910,7 +99415,7 @@ projectCmd.command("use <name>").description("Set active project (find or create
|
|
|
98910
99415
|
var repoCmd = program2.command("repo").description("Discover and run repo-native Playwright tests");
|
|
98911
99416
|
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
99417
|
try {
|
|
98913
|
-
const repoPath =
|
|
99418
|
+
const repoPath = resolve5(path ?? process.cwd());
|
|
98914
99419
|
const snapshot = discoverRepo({
|
|
98915
99420
|
repoPath,
|
|
98916
99421
|
refresh: opts.refresh,
|
|
@@ -98976,7 +99481,7 @@ repoCmd.command("discover [path]").alias("scan").description("Discover Playwrigh
|
|
|
98976
99481
|
});
|
|
98977
99482
|
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
99483
|
try {
|
|
98979
|
-
const repoPath =
|
|
99484
|
+
const repoPath = resolve5(path ?? process.cwd());
|
|
98980
99485
|
const snapshot = discoverRepo({ repoPath, refresh: opts.refresh });
|
|
98981
99486
|
const steps = [];
|
|
98982
99487
|
if (opts.all) {
|
|
@@ -99054,7 +99559,7 @@ repoCmd.command("run [path]").description("Run discovered Playwright tests nativ
|
|
|
99054
99559
|
return acc;
|
|
99055
99560
|
}, []).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
99561
|
try {
|
|
99057
|
-
const repoPath =
|
|
99562
|
+
const repoPath = resolve5(path ?? process.cwd());
|
|
99058
99563
|
const snapshot = discoverRepo({
|
|
99059
99564
|
repoPath,
|
|
99060
99565
|
refresh: opts.refresh,
|
|
@@ -99120,7 +99625,7 @@ repoCmd.command("run [path]").description("Run discovered Playwright tests nativ
|
|
|
99120
99625
|
repoCmd.command("cache [path]").description("Manage discovery cache").option("--clear", "Clear discovery cache", false).option("--status", "Show cache status", false).action((path, opts) => {
|
|
99121
99626
|
try {
|
|
99122
99627
|
if (opts.clear) {
|
|
99123
|
-
const repoPath2 = path ?
|
|
99628
|
+
const repoPath2 = path ? resolve5(path) : undefined;
|
|
99124
99629
|
clearDiscoveryCache(repoPath2);
|
|
99125
99630
|
if (repoPath2) {
|
|
99126
99631
|
log(chalk6.green("Discovery cache cleared for this repo."));
|
|
@@ -99130,7 +99635,7 @@ repoCmd.command("cache [path]").description("Manage discovery cache").option("--
|
|
|
99130
99635
|
return;
|
|
99131
99636
|
}
|
|
99132
99637
|
if (opts.status) {
|
|
99133
|
-
const repoPath2 =
|
|
99638
|
+
const repoPath2 = resolve5(path ?? process.cwd());
|
|
99134
99639
|
const info2 = getDiscoveryCacheInfo(repoPath2);
|
|
99135
99640
|
if (!info2) {
|
|
99136
99641
|
log(chalk6.dim("No discovery cache for this repo."));
|
|
@@ -99144,7 +99649,7 @@ repoCmd.command("cache [path]").description("Manage discovery cache").option("--
|
|
|
99144
99649
|
log("");
|
|
99145
99650
|
return;
|
|
99146
99651
|
}
|
|
99147
|
-
const repoPath =
|
|
99652
|
+
const repoPath = resolve5(path ?? process.cwd());
|
|
99148
99653
|
const info = getDiscoveryCacheInfo(repoPath);
|
|
99149
99654
|
if (!info) {
|
|
99150
99655
|
log(chalk6.dim("No discovery cache. Run 'testers repo discover' to create one."));
|
|
@@ -99233,8 +99738,8 @@ sessionCmd.command("show <id>").description("Show details of a recorded session"
|
|
|
99233
99738
|
});
|
|
99234
99739
|
sessionCmd.command("import <file>").description("Import a session JSON file exported from the Chrome extension").action(async (file2) => {
|
|
99235
99740
|
try {
|
|
99236
|
-
const { readFileSync:
|
|
99237
|
-
const raw =
|
|
99741
|
+
const { readFileSync: readFileSync12 } = await import("fs");
|
|
99742
|
+
const raw = readFileSync12(file2, "utf-8");
|
|
99238
99743
|
const data = JSON.parse(raw);
|
|
99239
99744
|
const items = Array.isArray(data) ? data : [data];
|
|
99240
99745
|
const { createSession: createSession2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
|
|
@@ -99474,7 +99979,7 @@ program2.command("daemon").description("Start the scheduler daemon").option("--i
|
|
|
99474
99979
|
} catch (err) {
|
|
99475
99980
|
logError(chalk6.red(`Daemon error: ${err instanceof Error ? err.message : String(err)}`));
|
|
99476
99981
|
}
|
|
99477
|
-
await new Promise((
|
|
99982
|
+
await new Promise((resolve6) => setTimeout(resolve6, intervalMs));
|
|
99478
99983
|
}
|
|
99479
99984
|
};
|
|
99480
99985
|
process.on("SIGINT", () => {
|
|
@@ -99503,9 +100008,9 @@ program2.command("ci [provider]").description("Print or write a CI workflow (def
|
|
|
99503
100008
|
}
|
|
99504
100009
|
const workflow = generateGitHubActionsWorkflow();
|
|
99505
100010
|
if (opts.output) {
|
|
99506
|
-
const outPath =
|
|
100011
|
+
const outPath = resolve5(opts.output);
|
|
99507
100012
|
const outDir = outPath.replace(/\/[^/]*$/, "");
|
|
99508
|
-
if (outDir && !
|
|
100013
|
+
if (outDir && !existsSync19(outDir)) {
|
|
99509
100014
|
mkdirSync15(outDir, { recursive: true });
|
|
99510
100015
|
}
|
|
99511
100016
|
writeFileSync7(outPath, workflow, "utf-8");
|
|
@@ -99539,11 +100044,11 @@ program2.command("init").description("Initialize a new testing project").option(
|
|
|
99539
100044
|
log(` ${chalk6.dim(s2.shortId)} ${s2.name} ${chalk6.dim(`[${s2.tags.join(", ")}]`)}`);
|
|
99540
100045
|
}
|
|
99541
100046
|
if (opts.ci === "github") {
|
|
99542
|
-
const workflowDir =
|
|
99543
|
-
if (!
|
|
100047
|
+
const workflowDir = join21(process.cwd(), ".github", "workflows");
|
|
100048
|
+
if (!existsSync19(workflowDir)) {
|
|
99544
100049
|
mkdirSync15(workflowDir, { recursive: true });
|
|
99545
100050
|
}
|
|
99546
|
-
const workflowPath =
|
|
100051
|
+
const workflowPath = join21(workflowDir, "testers.yml");
|
|
99547
100052
|
writeFileSync7(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
|
|
99548
100053
|
log(` CI: ${chalk6.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
|
|
99549
100054
|
} else if (opts.ci) {
|
|
@@ -99553,7 +100058,7 @@ program2.command("init").description("Initialize a new testing project").option(
|
|
|
99553
100058
|
if (opts.yes)
|
|
99554
100059
|
return;
|
|
99555
100060
|
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
99556
|
-
const ask = (q2) => new Promise((
|
|
100061
|
+
const ask = (q2) => new Promise((resolve6) => rl2.question(q2, resolve6));
|
|
99557
100062
|
try {
|
|
99558
100063
|
const envAnswer = await ask(" Would you like to configure environments? [y/N] ");
|
|
99559
100064
|
if (envAnswer.trim().toLowerCase() === "y") {
|
|
@@ -99737,14 +100242,14 @@ program2.command("quick-qa <url>").alias("quick-check").description("Run a fast
|
|
|
99737
100242
|
wcagLevel
|
|
99738
100243
|
});
|
|
99739
100244
|
if (opts.output) {
|
|
99740
|
-
writeFileSync7(
|
|
100245
|
+
writeFileSync7(resolve5(opts.output), JSON.stringify(result, null, 2));
|
|
99741
100246
|
}
|
|
99742
100247
|
if (opts.json) {
|
|
99743
100248
|
log(JSON.stringify(result, null, 2));
|
|
99744
100249
|
} else {
|
|
99745
100250
|
log(formatQuickQaReport(result));
|
|
99746
100251
|
if (opts.output)
|
|
99747
|
-
log(chalk6.dim(`Wrote JSON results to ${
|
|
100252
|
+
log(chalk6.dim(`Wrote JSON results to ${resolve5(opts.output)}`));
|
|
99748
100253
|
}
|
|
99749
100254
|
process.exit(getQuickQaExitCode(result));
|
|
99750
100255
|
} catch (error40) {
|
|
@@ -99789,7 +100294,7 @@ program2.command("report [run-id]").description("Generate HTML test report or co
|
|
|
99789
100294
|
});
|
|
99790
100295
|
if (opts.output && opts.output !== "report.html") {
|
|
99791
100296
|
writeFileSync7(opts.output, content, "utf-8");
|
|
99792
|
-
const absPath2 =
|
|
100297
|
+
const absPath2 = resolve5(opts.output);
|
|
99793
100298
|
log(chalk6.green(`Compliance report written to ${absPath2}`));
|
|
99794
100299
|
} else {
|
|
99795
100300
|
log(content);
|
|
@@ -99803,7 +100308,7 @@ program2.command("report [run-id]").description("Generate HTML test report or co
|
|
|
99803
100308
|
html = generateHtmlReport(runId);
|
|
99804
100309
|
}
|
|
99805
100310
|
writeFileSync7(opts.output, html, "utf-8");
|
|
99806
|
-
const absPath =
|
|
100311
|
+
const absPath = resolve5(opts.output);
|
|
99807
100312
|
log(chalk6.green(`Report generated: ${absPath}`));
|
|
99808
100313
|
if (opts.open) {
|
|
99809
100314
|
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
@@ -100160,6 +100665,62 @@ Imported ${imported} scenarios from API spec:`));
|
|
|
100160
100665
|
process.exit(1);
|
|
100161
100666
|
}
|
|
100162
100667
|
});
|
|
100668
|
+
var inventoryCmd = program2.command("inventory").description("Discover source-derived app route/action inventories");
|
|
100669
|
+
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) => {
|
|
100670
|
+
acc.push(val);
|
|
100671
|
+
return acc;
|
|
100672
|
+
}, []).option("--timeout <ms>", "Workflow timeout in milliseconds").option("--json", "Output as JSON", false).action(async (root, opts) => {
|
|
100673
|
+
try {
|
|
100674
|
+
const { importNextRouteInventory: importNextRouteInventory2 } = await Promise.resolve().then(() => (init_next_route_inventory(), exports_next_route_inventory));
|
|
100675
|
+
const projectId = resolveProject2(opts.project) ?? undefined;
|
|
100676
|
+
const env = parseSandboxEnv(undefined, opts.sandboxEnvOptional);
|
|
100677
|
+
const result = importNextRouteInventory2({
|
|
100678
|
+
rootDir: root ?? process.cwd(),
|
|
100679
|
+
appDir: opts.appDir,
|
|
100680
|
+
projectId,
|
|
100681
|
+
includePages: opts.pages !== false,
|
|
100682
|
+
includeApi: opts.api !== false,
|
|
100683
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
100684
|
+
createScenarios: opts.createScenarios,
|
|
100685
|
+
createWorkflows: opts.createWorkflows,
|
|
100686
|
+
workflowTarget: opts.workflowTarget,
|
|
100687
|
+
workflowProvider: opts.workflowTarget === "sandbox" ? opts.sandboxProvider : undefined,
|
|
100688
|
+
workflowExecution: {
|
|
100689
|
+
target: opts.workflowTarget,
|
|
100690
|
+
provider: opts.workflowTarget === "sandbox" ? opts.sandboxProvider : undefined,
|
|
100691
|
+
sandboxCleanup: opts.sandboxCleanup,
|
|
100692
|
+
sandboxSyncStrategy: opts.sandboxSync,
|
|
100693
|
+
timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
100694
|
+
env
|
|
100695
|
+
}
|
|
100696
|
+
});
|
|
100697
|
+
if (opts.json) {
|
|
100698
|
+
log(JSON.stringify(result, null, 2));
|
|
100699
|
+
return;
|
|
100700
|
+
}
|
|
100701
|
+
log("");
|
|
100702
|
+
log(chalk6.bold(" Next.js Route Inventory"));
|
|
100703
|
+
log(chalk6.dim(` Root: ${result.inventory.rootDir}`));
|
|
100704
|
+
log(chalk6.dim(` App: ${result.inventory.appDir}`));
|
|
100705
|
+
log("");
|
|
100706
|
+
log(` Routes: ${chalk6.cyan(String(result.inventory.total))} (${result.inventory.pages} pages, ${result.inventory.apiRoutes} API, ${result.inventory.dynamic} dynamic)`);
|
|
100707
|
+
log(` Scenarios: ${chalk6.green(String(result.created))} created, ${chalk6.yellow(String(result.updated))} updated, ${chalk6.dim(String(result.deduped))} deduped`);
|
|
100708
|
+
log(` Workflows: ${result.workflows.length}`);
|
|
100709
|
+
log("");
|
|
100710
|
+
for (const [category, count] of Object.entries(result.inventory.categories).sort()) {
|
|
100711
|
+
log(` ${category.padEnd(18)} ${count}`);
|
|
100712
|
+
}
|
|
100713
|
+
log("");
|
|
100714
|
+
if (!opts.createScenarios)
|
|
100715
|
+
log(chalk6.dim(" Add --create-scenarios to upsert route scenarios."));
|
|
100716
|
+
if (!opts.createWorkflows)
|
|
100717
|
+
log(chalk6.dim(" Add --create-workflows to upsert grouped workflows."));
|
|
100718
|
+
log("");
|
|
100719
|
+
} catch (error40) {
|
|
100720
|
+
logError(chalk6.red(`Error: ${error40 instanceof Error ? error40.message : String(error40)}`));
|
|
100721
|
+
process.exit(1);
|
|
100722
|
+
}
|
|
100723
|
+
});
|
|
100163
100724
|
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
100725
|
try {
|
|
100165
100726
|
const { generateScenarios: generateScenarios2 } = await Promise.resolve().then(() => (init_generator(), exports_generator));
|
|
@@ -100622,7 +101183,7 @@ program2.command("doctor").description("Check system setup and configuration").a
|
|
|
100622
101183
|
log(chalk6.red("\u2717") + " ANTHROPIC_API_KEY is not set (required for AI-powered tests)");
|
|
100623
101184
|
allPassed = false;
|
|
100624
101185
|
}
|
|
100625
|
-
const dbPath =
|
|
101186
|
+
const dbPath = join21(getTestersDir(), "testers.db");
|
|
100626
101187
|
try {
|
|
100627
101188
|
const { Database: Database5 } = await import("bun:sqlite");
|
|
100628
101189
|
const db3 = new Database5(dbPath, { create: true });
|
|
@@ -100676,7 +101237,7 @@ program2.command("serve").description("Start the Open Testers web dashboard").op
|
|
|
100676
101237
|
try {
|
|
100677
101238
|
const port = parseInt(opts.port, 10);
|
|
100678
101239
|
const url2 = `http://localhost:${port}`;
|
|
100679
|
-
const serverBin =
|
|
101240
|
+
const serverBin = join21(resolve5(process.execPath, ".."), "..", "dist", "server", "index.js");
|
|
100680
101241
|
const { join: pathJoin, resolve: pathResolve, dirname: dirname7 } = await import("path");
|
|
100681
101242
|
const { fileURLToPath: fileURLToPath2 } = await import("url");
|
|
100682
101243
|
const serverPath = pathJoin(dirname7(fileURLToPath2(import.meta.url)), "..", "server", "index.js");
|
|
@@ -101442,7 +102003,7 @@ personaCmd.command("delete <id>").description("Delete a persona").option("-y, --
|
|
|
101442
102003
|
}
|
|
101443
102004
|
if (!opts.yes) {
|
|
101444
102005
|
process.stdout.write(chalk6.yellow(`Delete persona ${persona.shortId} "${persona.name}"? [y/N] `));
|
|
101445
|
-
const answer = await new Promise((
|
|
102006
|
+
const answer = await new Promise((resolve6) => {
|
|
101446
102007
|
let buf = "";
|
|
101447
102008
|
process.stdin.setRawMode?.(true);
|
|
101448
102009
|
process.stdin.resume();
|
|
@@ -101452,7 +102013,7 @@ personaCmd.command("delete <id>").description("Delete a persona").option("-y, --
|
|
|
101452
102013
|
process.stdin.pause();
|
|
101453
102014
|
process.stdout.write(`
|
|
101454
102015
|
`);
|
|
101455
|
-
|
|
102016
|
+
resolve6(buf);
|
|
101456
102017
|
});
|
|
101457
102018
|
});
|
|
101458
102019
|
if (answer !== "y" && answer !== "yes") {
|
|
@@ -101613,7 +102174,7 @@ evalCmd.command("rag <url>").description("Run RAG quality evaluation \u2014 fait
|
|
|
101613
102174
|
let ragTestCases = [];
|
|
101614
102175
|
if (opts.docs) {
|
|
101615
102176
|
try {
|
|
101616
|
-
const raw =
|
|
102177
|
+
const raw = readFileSync11(opts.docs, "utf-8");
|
|
101617
102178
|
ragTestCases = JSON.parse(raw);
|
|
101618
102179
|
} catch {
|
|
101619
102180
|
logError(chalk6.red(`Failed to read docs file: ${opts.docs}`));
|
|
@@ -101703,9 +102264,9 @@ Created golden answer check ${chalk6.bold(golden2.shortId)}`));
|
|
|
101703
102264
|
}
|
|
101704
102265
|
const ask = (prompt) => {
|
|
101705
102266
|
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
101706
|
-
return new Promise((
|
|
102267
|
+
return new Promise((resolve6) => rl2.question(prompt, (ans) => {
|
|
101707
102268
|
rl2.close();
|
|
101708
|
-
|
|
102269
|
+
resolve6(ans.trim());
|
|
101709
102270
|
}));
|
|
101710
102271
|
};
|
|
101711
102272
|
const question = await ask("Question (what this endpoint should answer): ");
|
|
@@ -101866,9 +102427,9 @@ program2.command("run-many <url>").description("Run scenarios \xD7 personas matr
|
|
|
101866
102427
|
});
|
|
101867
102428
|
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
102429
|
try {
|
|
101869
|
-
const { resolve:
|
|
102430
|
+
const { resolve: resolve6 } = await import("path");
|
|
101870
102431
|
const { runHybridScenario: runHybridScenario2 } = await Promise.resolve().then(() => (init_hybrid_runner(), exports_hybrid_runner));
|
|
101871
|
-
const scriptPath =
|
|
102432
|
+
const scriptPath = resolve6(process.cwd(), file2);
|
|
101872
102433
|
const mod = await import(scriptPath);
|
|
101873
102434
|
const scenarios = mod.scenarios ?? mod.default ?? [];
|
|
101874
102435
|
if (!Array.isArray(scenarios) || scenarios.length === 0) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { CreateScenarioInput, Scenario, ScenarioPriority, TestingWorkflow, WorkflowExecutionInput } from "../types/index.js";
|
|
2
|
+
export type NextRouteKind = "page" | "api";
|
|
3
|
+
export type NextRouteActionKind = "link" | "button" | "form" | "input" | "api-method";
|
|
4
|
+
export interface NextRouteAction {
|
|
5
|
+
kind: NextRouteActionKind;
|
|
6
|
+
label: string;
|
|
7
|
+
target?: string;
|
|
8
|
+
sourceFile: string;
|
|
9
|
+
destructive: boolean;
|
|
10
|
+
requiresFixture: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface NextRouteInventoryItem {
|
|
13
|
+
kind: NextRouteKind;
|
|
14
|
+
routePath: string;
|
|
15
|
+
file: string;
|
|
16
|
+
category: string;
|
|
17
|
+
groups: string[];
|
|
18
|
+
methods: string[];
|
|
19
|
+
dynamic: boolean;
|
|
20
|
+
requiresAuth: boolean;
|
|
21
|
+
fixtureParams: string[];
|
|
22
|
+
actions: NextRouteAction[];
|
|
23
|
+
tags: string[];
|
|
24
|
+
priority: ScenarioPriority;
|
|
25
|
+
}
|
|
26
|
+
export interface NextRouteInventory {
|
|
27
|
+
rootDir: string;
|
|
28
|
+
appDir: string;
|
|
29
|
+
total: number;
|
|
30
|
+
pages: number;
|
|
31
|
+
apiRoutes: number;
|
|
32
|
+
dynamic: number;
|
|
33
|
+
categories: Record<string, number>;
|
|
34
|
+
items: NextRouteInventoryItem[];
|
|
35
|
+
}
|
|
36
|
+
export interface ImportNextRouteInventoryOptions {
|
|
37
|
+
rootDir: string;
|
|
38
|
+
appDir?: string;
|
|
39
|
+
projectId?: string;
|
|
40
|
+
includePages?: boolean;
|
|
41
|
+
includeApi?: boolean;
|
|
42
|
+
limit?: number;
|
|
43
|
+
createScenarios?: boolean;
|
|
44
|
+
createWorkflows?: boolean;
|
|
45
|
+
workflowTarget?: "local" | "sandbox";
|
|
46
|
+
workflowProvider?: string;
|
|
47
|
+
workflowExecution?: Partial<WorkflowExecutionInput>;
|
|
48
|
+
}
|
|
49
|
+
export interface ImportNextRouteInventoryResult {
|
|
50
|
+
inventory: NextRouteInventory;
|
|
51
|
+
created: number;
|
|
52
|
+
updated: number;
|
|
53
|
+
deduped: number;
|
|
54
|
+
scenarios: Scenario[];
|
|
55
|
+
workflows: TestingWorkflow[];
|
|
56
|
+
}
|
|
57
|
+
export declare function discoverNextRouteInventory(options: {
|
|
58
|
+
rootDir: string;
|
|
59
|
+
appDir?: string;
|
|
60
|
+
includePages?: boolean;
|
|
61
|
+
includeApi?: boolean;
|
|
62
|
+
limit?: number;
|
|
63
|
+
}): NextRouteInventory;
|
|
64
|
+
export declare function scenarioInputForNextRoute(item: NextRouteInventoryItem, projectId?: string): CreateScenarioInput;
|
|
65
|
+
export declare function importNextRouteInventory(options: ImportNextRouteInventoryOptions): ImportNextRouteInventoryResult;
|
|
66
|
+
//# 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;AAC3C,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,YAAY,CAAC;AAEtF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,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,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,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;AAkCD,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,CAwDrB;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.
|
|
55
|
+
version: "0.0.50",
|
|
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",
|
package/dist/server/index.js
CHANGED
|
@@ -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.
|
|
46940
|
+
version: "0.0.50",
|
|
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",
|