@hasna/testers 0.0.51 → 0.0.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,7 +41,28 @@ testers workflow fanout --project alumia --workers 6 --url https://preview.examp
41
41
  testers workflow fanout wf_abc,wf_def wf_xyz --workers 12 --url https://preview.example.com --json
42
42
  ```
43
43
 
44
- `--workers` is bounded to 1-12 concurrent sandboxes. Use `--dry-run` to inspect the remote commands and upload plans without spawning sandboxes.
44
+ `--workers` is bounded to 1-12 concurrent sandboxes. Fanout preflights provider credentials, required sandbox environment references, `rsync`, and app source directories before launching workers. Use `--dry-run` to inspect the remote commands, upload plans, and preflight checks without spawning sandboxes.
45
+
46
+ ### Next.js Route and Action Inventory
47
+
48
+ For large apps, generate source-derived route coverage from the Next.js app directory. The importer can create route-level scenarios and one scenario per discovered link, button, form, input, or API method, then group those action scenarios into sandbox workflows for fanout.
49
+
50
+ ```bash
51
+ testers inventory next /path/to/app \
52
+ --project alumia \
53
+ --create-scenarios \
54
+ --create-action-scenarios \
55
+ --create-workflows \
56
+ --create-action-workflows \
57
+ --action-workflow-grouping route \
58
+ --sandbox-provider e2b \
59
+ --sandbox-sync rsync \
60
+ --sandbox-env-optional OPENAI_API_KEY
61
+
62
+ testers workflow fanout --project alumia --tag next-action --workers 6 --url https://preview.example.com --dry-run
63
+ ```
64
+
65
+ Use `--action-workflow-grouping route` for route-specific workflows or `--action-workflow-grouping area-kind` for broader workflows such as commerce buttons or admin API methods.
45
66
 
46
67
  ### Common Flags
47
68
 
package/dist/cli/index.js CHANGED
@@ -59331,6 +59331,7 @@ var init_openapi_import = __esm(() => {
59331
59331
  // src/lib/next-route-inventory.ts
59332
59332
  var exports_next_route_inventory = {};
59333
59333
  __export(exports_next_route_inventory, {
59334
+ scenarioInputsForNextRouteActions: () => scenarioInputsForNextRouteActions,
59334
59335
  scenarioInputForNextRoute: () => scenarioInputForNextRoute,
59335
59336
  importNextRouteInventory: () => importNextRouteInventory,
59336
59337
  discoverNextRouteInventory: () => discoverNextRouteInventory
@@ -59355,6 +59356,7 @@ function discoverNextRouteInventory(options) {
59355
59356
  pages: items.filter((item) => item.kind === "page").length,
59356
59357
  apiRoutes: items.filter((item) => item.kind === "api").length,
59357
59358
  dynamic: items.filter((item) => item.dynamic).length,
59359
+ actions: items.reduce((sum, item) => sum + item.actions.length, 0),
59358
59360
  categories,
59359
59361
  items
59360
59362
  };
@@ -59410,12 +59412,69 @@ function scenarioInputForNextRoute(item, projectId) {
59410
59412
  projectId
59411
59413
  };
59412
59414
  }
59415
+ function scenarioInputsForNextRouteActions(item, projectId) {
59416
+ return item.actions.map((action, index) => scenarioInputForNextRouteAction(item, action, index, projectId));
59417
+ }
59418
+ function scenarioInputForNextRouteAction(item, action, index, projectId) {
59419
+ const label = item.kind === "page" ? "page action" : "API action";
59420
+ const fixtureStep = item.fixtureParams.length > 0 ? `Bind dynamic fixture values for ${item.fixtureParams.map((name) => `:${name}`).join(", ")} before exercising this action.` : undefined;
59421
+ const dynamicStep = item.dynamic ? "Substitute dynamic path parameters with valid fixture values from the target org before opening or calling the route." : undefined;
59422
+ const destructiveGuard = action.destructive ? "If the action reaches a destructive confirmation or mutating final step, verify the warning/cancel path and stop before confirming." : undefined;
59423
+ const pageSteps = [
59424
+ fixtureStep,
59425
+ dynamicStep,
59426
+ `Open the Next.js page ${item.routePath}.`,
59427
+ "Wait for the route to finish loading and verify it does not show a blank shell, framework error page, or unexpected auth loop.",
59428
+ `Locate source-discovered ${action.kind} "${action.label}" from ${action.sourceFile}.`,
59429
+ formatActionStep(action),
59430
+ destructiveGuard,
59431
+ action.kind === "input" ? "Fill the input with safe test data and verify the UI accepts, validates, or rejects it predictably." : "Verify the action produces the expected navigation, modal, toast, table change, validation state, or disabled state.",
59432
+ "Verify the route stays within the expected org/workspace context and does not emit console errors."
59433
+ ].filter(Boolean);
59434
+ const apiSteps = [
59435
+ fixtureStep,
59436
+ dynamicStep,
59437
+ `Call ${action.label} ${item.routePath} using safe fixture data.`,
59438
+ action.destructive ? "Use a harmless dry-run/no-op fixture when available; otherwise verify authentication, authorization, validation, or confirmation blocks without creating destructive side effects." : "Verify the method accepts valid safe input and rejects invalid input with a stable response shape.",
59439
+ "Verify expected authentication, authorization, validation, and tenant isolation behavior.",
59440
+ "Verify response status, JSON shape, and error messages are stable and regression-safe."
59441
+ ].filter(Boolean);
59442
+ return {
59443
+ name: `Next ${label}: ${item.routePath} :: ${action.kind} ${action.label}`,
59444
+ description: `Source-discovered ${label} ${index + 1} from ${action.sourceFile}. Verify ${action.kind} "${action.label}" on ${item.routePath}.`,
59445
+ steps: item.kind === "page" ? pageSteps : apiSteps,
59446
+ tags: actionTagsForRoute(item, action),
59447
+ priority: action.destructive ? "critical" : item.priority,
59448
+ targetPath: item.routePath,
59449
+ requiresAuth: item.requiresAuth,
59450
+ assertions: item.kind === "page" ? SAFE_PAGE_ASSERTIONS : [],
59451
+ metadata: {
59452
+ source: "next-route-action-inventory",
59453
+ routeFile: item.file,
59454
+ routeKind: item.kind,
59455
+ routePath: item.routePath,
59456
+ category: item.category,
59457
+ methods: item.methods,
59458
+ dynamic: item.dynamic,
59459
+ fixtureParams: item.fixtureParams,
59460
+ actionIndex: index,
59461
+ action,
59462
+ groups: item.groups
59463
+ },
59464
+ parameters: item.fixtureParams.length > 0 ? {
59465
+ routeFixtures: defaultRouteFixturesForParams(item.fixtureParams),
59466
+ routeFixtureParams: item.fixtureParams
59467
+ } : undefined,
59468
+ projectId
59469
+ };
59470
+ }
59413
59471
  function importNextRouteInventory(options) {
59414
59472
  const inventory = discoverNextRouteInventory(options);
59415
59473
  let created = 0;
59416
59474
  let updated = 0;
59417
59475
  let deduped = 0;
59418
59476
  const scenarios = [];
59477
+ const actionScenarios = [];
59419
59478
  const workflows = [];
59420
59479
  if (options.createScenarios) {
59421
59480
  for (const item of inventory.items) {
@@ -59429,10 +59488,28 @@ function importNextRouteInventory(options) {
59429
59488
  deduped++;
59430
59489
  }
59431
59490
  }
59491
+ if (options.createActionScenarios) {
59492
+ for (const item of inventory.items) {
59493
+ for (const input of scenarioInputsForNextRouteActions(item, options.projectId)) {
59494
+ const result = upsertScenario(input);
59495
+ scenarios.push(result.scenario);
59496
+ actionScenarios.push(result.scenario);
59497
+ if (result.action === "created")
59498
+ created++;
59499
+ else if (result.action === "updated")
59500
+ updated++;
59501
+ else
59502
+ deduped++;
59503
+ }
59504
+ }
59505
+ }
59432
59506
  if (options.createWorkflows) {
59433
59507
  workflows.push(...upsertRouteInventoryWorkflows(inventory, options));
59434
59508
  }
59435
- return { inventory, created, updated, deduped, scenarios, workflows };
59509
+ if (options.createActionWorkflows) {
59510
+ workflows.push(...upsertRouteInventoryActionWorkflows(inventory, options));
59511
+ }
59512
+ return { inventory, created, updated, deduped, scenarios, actionScenarios, workflows };
59436
59513
  }
59437
59514
  function upsertRouteInventoryWorkflows(inventory, options) {
59438
59515
  const workflows = [];
@@ -59462,6 +59539,57 @@ function upsertRouteInventoryWorkflows(inventory, options) {
59462
59539
  }
59463
59540
  return workflows;
59464
59541
  }
59542
+ function upsertRouteInventoryActionWorkflows(inventory, options) {
59543
+ const workflows = [];
59544
+ const grouping = options.actionWorkflowGrouping ?? "route";
59545
+ const existingWorkflows = listTestingWorkflows({ projectId: options.projectId, enabled: undefined });
59546
+ if (grouping === "area-kind") {
59547
+ const keys = new Set;
59548
+ for (const item of inventory.items) {
59549
+ for (const action of item.actions) {
59550
+ keys.add(`${item.category}|${action.kind}`);
59551
+ }
59552
+ }
59553
+ for (const key of [...keys].sort()) {
59554
+ const [category, actionKind] = key.split("|");
59555
+ const name = `Next action inventory ${category} ${actionKind}`;
59556
+ const scenarioTags = ["next-action", `area:${category}`, `action:${actionKind}`];
59557
+ workflows.push(upsertTestingWorkflow(existingWorkflows, name, {
59558
+ name,
59559
+ description: `Source-discovered ${actionKind} action coverage for ${category} routes.`,
59560
+ projectId: options.projectId,
59561
+ scenarioFilter: { tags: scenarioTags },
59562
+ execution: workflowExecutionFromOptions(options)
59563
+ }));
59564
+ }
59565
+ return workflows;
59566
+ }
59567
+ for (const item of inventory.items.filter((route) => route.actions.length > 0)) {
59568
+ const name = `Next action inventory ${item.kind} ${item.routePath}`;
59569
+ const scenarioTags = ["next-action", `route:${item.kind}`, `route-path:${item.routePath}`];
59570
+ workflows.push(upsertTestingWorkflow(existingWorkflows, name, {
59571
+ name,
59572
+ description: `Source-discovered action coverage for ${item.kind} route ${item.routePath}.`,
59573
+ projectId: options.projectId,
59574
+ scenarioFilter: { tags: scenarioTags },
59575
+ execution: workflowExecutionFromOptions(options)
59576
+ }));
59577
+ }
59578
+ return workflows;
59579
+ }
59580
+ function workflowExecutionFromOptions(options) {
59581
+ return {
59582
+ target: options.workflowTarget ?? "sandbox",
59583
+ provider: options.workflowProvider,
59584
+ sandboxCleanup: "delete",
59585
+ sandboxSyncStrategy: "rsync",
59586
+ ...options.workflowExecution
59587
+ };
59588
+ }
59589
+ function upsertTestingWorkflow(existingWorkflows, name, input) {
59590
+ const existing = existingWorkflows.find((workflow) => workflow.name === name);
59591
+ return existing ? updateTestingWorkflow(existing.id, input) : createTestingWorkflow(input);
59592
+ }
59465
59593
  function resolveAppDir(rootDir, appDir) {
59466
59594
  const candidates = appDir ? [resolve3(rootDir, appDir)] : [
59467
59595
  join20(rootDir, "packages", "web", "app"),
@@ -59792,6 +59920,19 @@ function tagsForRoute(input) {
59792
59920
  tags.add("api");
59793
59921
  return [...tags];
59794
59922
  }
59923
+ function actionTagsForRoute(item, action) {
59924
+ const tags = new Set([
59925
+ ...item.tags,
59926
+ "next-action",
59927
+ `action:${action.kind}`,
59928
+ `route-path:${item.routePath}`
59929
+ ]);
59930
+ if (action.destructive)
59931
+ tags.add("destructive-action");
59932
+ if (action.requiresFixture)
59933
+ tags.add("fixture-required");
59934
+ return [...tags];
59935
+ }
59795
59936
  function priorityForRoute(routePath, category, kind) {
59796
59937
  if (category === "auth")
59797
59938
  return "critical";
@@ -60463,8 +60604,11 @@ var exports_workflow_fanout = {};
60463
60604
  __export(exports_workflow_fanout, {
60464
60605
  runWorkflowFanout: () => runWorkflowFanout,
60465
60606
  resolveWorkflowFanoutSelection: () => resolveWorkflowFanoutSelection,
60466
- normalizeFanoutWorkerCount: () => normalizeFanoutWorkerCount
60607
+ normalizeFanoutWorkerCount: () => normalizeFanoutWorkerCount,
60608
+ checkWorkflowFanoutReadiness: () => checkWorkflowFanoutReadiness
60467
60609
  });
60610
+ import { spawnSync as spawnSync2 } from "child_process";
60611
+ import { existsSync as existsSync19, statSync as statSync5 } from "fs";
60468
60612
  function splitWorkflowIds(ids) {
60469
60613
  return (ids ?? []).flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
60470
60614
  }
@@ -60497,6 +60641,89 @@ function resolveWorkflowFanoutSelection(options) {
60497
60641
  }
60498
60642
  return filtered;
60499
60643
  }
60644
+ async function checkWorkflowFanoutReadiness(workflows, dependencies = {}) {
60645
+ const checks = [];
60646
+ const env = dependencies.env ?? process.env;
60647
+ for (const [provider, providerWorkflows] of groupWorkflowsByProvider(workflows)) {
60648
+ const envKey = PROVIDER_ENV_KEYS[provider];
60649
+ if (!envKey) {
60650
+ checks.push({
60651
+ name: `provider:${provider}`,
60652
+ ok: true,
60653
+ required: false,
60654
+ message: `No built-in credential preflight for sandbox provider "${provider}"`,
60655
+ workflows: providerWorkflows.map((workflow) => workflow.name)
60656
+ });
60657
+ continue;
60658
+ }
60659
+ const apiKey = await resolveProviderApiKey(provider, env, dependencies.providerApiKeyResolver);
60660
+ checks.push({
60661
+ name: `provider:${provider}`,
60662
+ ok: Boolean(apiKey),
60663
+ required: true,
60664
+ message: apiKey ? `Sandbox provider "${provider}" credential is available` : `Missing sandbox provider credential for "${provider}". Set ${envKey} or configure providers.${provider}.api_key with sandboxes config`,
60665
+ workflows: providerWorkflows.map((workflow) => workflow.name),
60666
+ details: { provider, envKey }
60667
+ });
60668
+ }
60669
+ const needsRsync = workflows.filter((workflow) => (workflow.execution.sandboxSyncStrategy ?? "rsync") === "rsync" || Boolean(workflow.execution.appSourceDir));
60670
+ if (needsRsync.length > 0) {
60671
+ const commandExists = dependencies.commandExists ?? defaultCommandExists;
60672
+ const rsyncOk = commandExists("rsync");
60673
+ checks.push({
60674
+ name: "tool:rsync",
60675
+ ok: rsyncOk,
60676
+ required: true,
60677
+ message: rsyncOk ? "rsync is available for sandbox uploads and app source bundling" : "Missing rsync. Install rsync or use --sandbox-sync archive for workflows without app source bundling",
60678
+ workflows: needsRsync.map((workflow) => workflow.name)
60679
+ });
60680
+ }
60681
+ const missingAppSources = workflows.filter((workflow) => workflow.execution.appSourceDir).filter((workflow) => {
60682
+ const sourceDir = workflow.execution.appSourceDir;
60683
+ return !existsSync19(sourceDir) || !statSync5(sourceDir).isDirectory();
60684
+ });
60685
+ if (missingAppSources.length > 0) {
60686
+ checks.push({
60687
+ name: "app-source",
60688
+ ok: false,
60689
+ required: true,
60690
+ message: "One or more workflow app source directories are missing or are not directories",
60691
+ workflows: missingAppSources.map((workflow) => workflow.name),
60692
+ details: {
60693
+ sources: missingAppSources.map((workflow) => ({
60694
+ workflowId: workflow.id,
60695
+ workflowName: workflow.name,
60696
+ appSourceDir: workflow.execution.appSourceDir
60697
+ }))
60698
+ }
60699
+ });
60700
+ }
60701
+ const { requiredMissing, optionalMissing } = collectMissingSandboxEnvRefs(workflows, env, dependencies.credentialResolver);
60702
+ if (requiredMissing.length > 0) {
60703
+ checks.push({
60704
+ name: "env:required",
60705
+ ok: false,
60706
+ required: true,
60707
+ message: "One or more required sandbox environment references could not be resolved",
60708
+ workflows: [...new Set(requiredMissing.map((item) => item.workflowName))],
60709
+ details: { missing: requiredMissing }
60710
+ });
60711
+ }
60712
+ if (optionalMissing.length > 0) {
60713
+ checks.push({
60714
+ name: "env:optional",
60715
+ ok: false,
60716
+ required: false,
60717
+ message: "One or more optional sandbox environment references are not set and will be omitted",
60718
+ workflows: [...new Set(optionalMissing.map((item) => item.workflowName))],
60719
+ details: { missing: optionalMissing }
60720
+ });
60721
+ }
60722
+ return {
60723
+ ok: checks.every((check) => check.ok || !check.required),
60724
+ checks
60725
+ };
60726
+ }
60500
60727
  async function mapWithConcurrency(items, limit, worker) {
60501
60728
  const output = new Array(items.length);
60502
60729
  let next = 0;
@@ -60512,7 +60739,38 @@ async function mapWithConcurrency(items, limit, worker) {
60512
60739
  async function runWorkflowFanout(options, dependencies = {}) {
60513
60740
  const workers = normalizeFanoutWorkerCount(options.workers);
60514
60741
  const workflows = resolveWorkflowFanoutSelection(options);
60515
- const { runTestingWorkflow: runOne = runTestingWorkflow, ...workflowDependencies } = dependencies;
60742
+ const {
60743
+ runTestingWorkflow: runOne = runTestingWorkflow,
60744
+ preflight: preflightOverride,
60745
+ providerApiKeyResolver,
60746
+ commandExists,
60747
+ credentialResolver,
60748
+ env,
60749
+ ...workflowDependencies
60750
+ } = dependencies;
60751
+ const preflight = preflightOverride ? await preflightOverride(workflows) : await checkWorkflowFanoutReadiness(workflows, {
60752
+ providerApiKeyResolver,
60753
+ commandExists,
60754
+ credentialResolver,
60755
+ env
60756
+ });
60757
+ if (!options.dryRun && !preflight.ok) {
60758
+ const error = `Preflight failed: ${summarizePreflightFailures(preflight)}`;
60759
+ return {
60760
+ status: "failed",
60761
+ workers,
60762
+ total: workflows.length,
60763
+ passed: 0,
60764
+ failed: workflows.length,
60765
+ preflight,
60766
+ items: workflows.map((workflow) => ({
60767
+ workflowId: workflow.id,
60768
+ workflowName: workflow.name,
60769
+ status: "failed",
60770
+ error
60771
+ }))
60772
+ };
60773
+ }
60516
60774
  const items = await mapWithConcurrency(workflows, workers, async (workflow) => {
60517
60775
  try {
60518
60776
  const output = await runOne(workflow.id, {
@@ -60559,12 +60817,81 @@ async function runWorkflowFanout(options, dependencies = {}) {
60559
60817
  total: items.length,
60560
60818
  passed,
60561
60819
  failed,
60562
- items
60820
+ items,
60821
+ preflight
60563
60822
  };
60564
60823
  }
60824
+ function groupWorkflowsByProvider(workflows) {
60825
+ const byProvider = new Map;
60826
+ for (const workflow of workflows) {
60827
+ const provider = workflow.execution.provider ?? "e2b";
60828
+ byProvider.set(provider, [...byProvider.get(provider) ?? [], workflow]);
60829
+ }
60830
+ return byProvider;
60831
+ }
60832
+ async function resolveProviderApiKey(provider, env, resolver) {
60833
+ if (resolver)
60834
+ return resolver(provider, env);
60835
+ const envKey = PROVIDER_ENV_KEYS[provider];
60836
+ if (envKey && env[envKey])
60837
+ return env[envKey];
60838
+ try {
60839
+ const mod = await import("@hasna/sandboxes");
60840
+ if (provider === "e2b" || provider === "daytona" || provider === "modal") {
60841
+ return mod.getProviderApiKey?.(provider);
60842
+ }
60843
+ } catch {}
60844
+ return;
60845
+ }
60846
+ function defaultCommandExists(command) {
60847
+ const result = spawnSync2(command, ["--version"], { encoding: "utf8" });
60848
+ return !result.error && result.status === 0;
60849
+ }
60850
+ function collectMissingSandboxEnvRefs(workflows, env, credentialResolver) {
60851
+ const requiredMissing = [];
60852
+ const optionalMissing = [];
60853
+ for (const workflow of workflows) {
60854
+ for (const [key, value] of Object.entries(workflow.execution.env ?? {})) {
60855
+ if (value.startsWith("$?")) {
60856
+ const name = value.slice(2).trim();
60857
+ if (name && env[name] === undefined) {
60858
+ optionalMissing.push({ workflowId: workflow.id, workflowName: workflow.name, key, reference: value });
60859
+ }
60860
+ continue;
60861
+ }
60862
+ if (!isResolvableEnvReference(value))
60863
+ continue;
60864
+ if (resolveSandboxEnvReference(value, env, credentialResolver) === null) {
60865
+ requiredMissing.push({ workflowId: workflow.id, workflowName: workflow.name, key, reference: value });
60866
+ }
60867
+ }
60868
+ }
60869
+ return { requiredMissing, optionalMissing };
60870
+ }
60871
+ function isResolvableEnvReference(value) {
60872
+ return value.startsWith("$") || value.startsWith("@secrets:");
60873
+ }
60874
+ function resolveSandboxEnvReference(value, env, credentialResolver) {
60875
+ if (value.startsWith("$")) {
60876
+ const varName = value.slice(1).trim();
60877
+ return varName ? env[varName] ?? null : null;
60878
+ }
60879
+ return (credentialResolver ?? resolveCredential)(value);
60880
+ }
60881
+ function summarizePreflightFailures(preflight) {
60882
+ const requiredFailures = preflight.checks.filter((check) => !check.ok && check.required);
60883
+ return requiredFailures.length > 0 ? requiredFailures.map((check) => check.message).join("; ") : "required checks did not pass";
60884
+ }
60885
+ var PROVIDER_ENV_KEYS;
60565
60886
  var init_workflow_fanout = __esm(() => {
60566
60887
  init_workflows();
60567
60888
  init_workflow_runner();
60889
+ init_secrets_resolver();
60890
+ PROVIDER_ENV_KEYS = {
60891
+ e2b: "E2B_API_KEY",
60892
+ daytona: "DAYTONA_API_KEY",
60893
+ modal: "MODAL_TOKEN_ID"
60894
+ };
60568
60895
  });
60569
60896
 
60570
60897
  // node_modules/@ai-sdk/provider/dist/index.mjs
@@ -95224,7 +95551,7 @@ import chalk6 from "chalk";
95224
95551
  // package.json
95225
95552
  var package_default = {
95226
95553
  name: "@hasna/testers",
95227
- version: "0.0.51",
95554
+ version: "0.0.53",
95228
95555
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
95229
95556
  type: "module",
95230
95557
  main: "dist/index.js",
@@ -97411,7 +97738,7 @@ init_ci();
97411
97738
  init_assertions();
97412
97739
  init_paths();
97413
97740
  init_sessions();
97414
- import { existsSync as existsSync19, mkdirSync as mkdirSync15 } from "fs";
97741
+ import { existsSync as existsSync20, mkdirSync as mkdirSync15 } from "fs";
97415
97742
 
97416
97743
  // src/lib/repo-discovery.ts
97417
97744
  init_paths();
@@ -98440,7 +98767,7 @@ var CONFIG_DIR5 = getTestersDir();
98440
98767
  var CONFIG_PATH4 = join21(CONFIG_DIR5, "config.json");
98441
98768
  function getActiveProject() {
98442
98769
  try {
98443
- if (existsSync19(CONFIG_PATH4)) {
98770
+ if (existsSync20(CONFIG_PATH4)) {
98444
98771
  const raw = JSON.parse(readFileSync11(CONFIG_PATH4, "utf-8"));
98445
98772
  return raw.activeProject ?? undefined;
98446
98773
  }
@@ -99371,7 +99698,7 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
99371
99698
  return;
99372
99699
  }
99373
99700
  const outputDir = opts.output ?? ".";
99374
- if (!existsSync19(outputDir)) {
99701
+ if (!existsSync20(outputDir)) {
99375
99702
  mkdirSync15(outputDir, { recursive: true });
99376
99703
  }
99377
99704
  for (const s2 of scenarios) {
@@ -99574,11 +99901,11 @@ projectCmd.command("export-open <id>").description("Register a testers project i
99574
99901
  projectCmd.command("use <name>").description("Set active project (find or create)").option("--json", "Output as JSON", false).action((name21, opts) => {
99575
99902
  try {
99576
99903
  const project = ensureProject(name21, process.cwd());
99577
- if (!existsSync19(CONFIG_DIR5)) {
99904
+ if (!existsSync20(CONFIG_DIR5)) {
99578
99905
  mkdirSync15(CONFIG_DIR5, { recursive: true });
99579
99906
  }
99580
99907
  let config2 = {};
99581
- if (existsSync19(CONFIG_PATH4)) {
99908
+ if (existsSync20(CONFIG_PATH4)) {
99582
99909
  try {
99583
99910
  config2 = JSON.parse(readFileSync11(CONFIG_PATH4, "utf-8"));
99584
99911
  } catch {}
@@ -100193,7 +100520,7 @@ program2.command("ci [provider]").description("Print or write a CI workflow (def
100193
100520
  if (opts.output) {
100194
100521
  const outPath = resolve5(opts.output);
100195
100522
  const outDir = outPath.replace(/\/[^/]*$/, "");
100196
- if (outDir && !existsSync19(outDir)) {
100523
+ if (outDir && !existsSync20(outDir)) {
100197
100524
  mkdirSync15(outDir, { recursive: true });
100198
100525
  }
100199
100526
  writeFileSync7(outPath, workflow, "utf-8");
@@ -100228,7 +100555,7 @@ program2.command("init").description("Initialize a new testing project").option(
100228
100555
  }
100229
100556
  if (opts.ci === "github") {
100230
100557
  const workflowDir = join21(process.cwd(), ".github", "workflows");
100231
- if (!existsSync19(workflowDir)) {
100558
+ if (!existsSync20(workflowDir)) {
100232
100559
  mkdirSync15(workflowDir, { recursive: true });
100233
100560
  }
100234
100561
  const workflowPath = join21(workflowDir, "testers.yml");
@@ -100849,7 +101176,7 @@ Imported ${imported} scenarios from API spec:`));
100849
101176
  }
100850
101177
  });
100851
101178
  var inventoryCmd = program2.command("inventory").description("Discover source-derived app route/action inventories");
100852
- 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) => {
101179
+ 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-action-scenarios", "Upsert one source-derived scenario per discovered page/API action", false).option("--create-workflows", "Upsert grouped workflows by area and route kind", false).option("--create-action-workflows", "Upsert action-focused workflows for discovered action scenarios", false).option("--action-workflow-grouping <mode>", "Action workflow grouping: route or area-kind", "route").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) => {
100853
101180
  acc.push(val);
100854
101181
  return acc;
100855
101182
  }, []).option("--timeout <ms>", "Workflow timeout in milliseconds").option("--json", "Output as JSON", false).action(async (root, opts) => {
@@ -100857,6 +101184,9 @@ inventoryCmd.command("next [root]").description("Discover Next.js app routes and
100857
101184
  const { importNextRouteInventory: importNextRouteInventory2 } = await Promise.resolve().then(() => (init_next_route_inventory(), exports_next_route_inventory));
100858
101185
  const projectId = resolveProject2(opts.project) ?? undefined;
100859
101186
  const env = parseSandboxEnv(undefined, opts.sandboxEnvOptional);
101187
+ if (!["route", "area-kind"].includes(opts.actionWorkflowGrouping)) {
101188
+ throw new Error("--action-workflow-grouping must be route or area-kind");
101189
+ }
100860
101190
  const result = importNextRouteInventory2({
100861
101191
  rootDir: root ?? process.cwd(),
100862
101192
  appDir: opts.appDir,
@@ -100865,7 +101195,10 @@ inventoryCmd.command("next [root]").description("Discover Next.js app routes and
100865
101195
  includeApi: opts.api !== false,
100866
101196
  limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
100867
101197
  createScenarios: opts.createScenarios,
101198
+ createActionScenarios: opts.createActionScenarios,
100868
101199
  createWorkflows: opts.createWorkflows,
101200
+ createActionWorkflows: opts.createActionWorkflows,
101201
+ actionWorkflowGrouping: opts.actionWorkflowGrouping,
100869
101202
  workflowTarget: opts.workflowTarget,
100870
101203
  workflowProvider: opts.workflowTarget === "sandbox" ? opts.sandboxProvider : undefined,
100871
101204
  workflowExecution: {
@@ -100887,6 +101220,7 @@ inventoryCmd.command("next [root]").description("Discover Next.js app routes and
100887
101220
  log(chalk6.dim(` App: ${result.inventory.appDir}`));
100888
101221
  log("");
100889
101222
  log(` Routes: ${chalk6.cyan(String(result.inventory.total))} (${result.inventory.pages} pages, ${result.inventory.apiRoutes} API, ${result.inventory.dynamic} dynamic)`);
101223
+ log(` Actions: ${chalk6.cyan(String(result.inventory.actions))}`);
100890
101224
  log(` Scenarios: ${chalk6.green(String(result.created))} created, ${chalk6.yellow(String(result.updated))} updated, ${chalk6.dim(String(result.deduped))} deduped`);
100891
101225
  log(` Workflows: ${result.workflows.length}`);
100892
101226
  log("");
@@ -100896,8 +101230,12 @@ inventoryCmd.command("next [root]").description("Discover Next.js app routes and
100896
101230
  log("");
100897
101231
  if (!opts.createScenarios)
100898
101232
  log(chalk6.dim(" Add --create-scenarios to upsert route scenarios."));
101233
+ if (!opts.createActionScenarios)
101234
+ log(chalk6.dim(" Add --create-action-scenarios to upsert one scenario per discovered action."));
100899
101235
  if (!opts.createWorkflows)
100900
101236
  log(chalk6.dim(" Add --create-workflows to upsert grouped workflows."));
101237
+ if (!opts.createActionWorkflows)
101238
+ log(chalk6.dim(" Add --create-action-workflows to upsert action-focused workflows."));
100901
101239
  log("");
100902
101240
  } catch (error40) {
100903
101241
  logError(chalk6.red(`Error: ${error40 instanceof Error ? error40.message : String(error40)}`));
@@ -102004,6 +102342,19 @@ workflowCmd.command("fanout [ids...]").description("Run multiple saved sandbox w
102004
102342
  if (opts.json || opts.dryRun) {
102005
102343
  log(JSON.stringify(result, null, 2));
102006
102344
  } else {
102345
+ const preflightChecks = result.preflight?.checks ?? [];
102346
+ const failedRequiredChecks = preflightChecks.filter((check2) => !check2.ok && check2.required);
102347
+ const warnings = preflightChecks.filter((check2) => !check2.ok && !check2.required);
102348
+ if (failedRequiredChecks.length > 0 || warnings.length > 0) {
102349
+ const label = failedRequiredChecks.length > 0 ? chalk6.red("failed") : chalk6.yellow("warnings");
102350
+ log(chalk6.bold(`Preflight ${label}:`));
102351
+ for (const check2 of failedRequiredChecks) {
102352
+ log(` ${chalk6.red("failed")} ${check2.message}`);
102353
+ }
102354
+ for (const check2 of warnings) {
102355
+ log(` ${chalk6.yellow("warning")} ${check2.message}`);
102356
+ }
102357
+ }
102007
102358
  const status = result.status === "passed" ? chalk6.green("passed") : chalk6.red("failed");
102008
102359
  log(chalk6.bold(`Sandbox workflow fanout ${status}: ${result.passed}/${result.total} passed with ${result.workers} worker(s)`));
102009
102360
  for (const item of result.items) {
@@ -30,6 +30,7 @@ export interface NextRouteInventory {
30
30
  pages: number;
31
31
  apiRoutes: number;
32
32
  dynamic: number;
33
+ actions: number;
33
34
  categories: Record<string, number>;
34
35
  items: NextRouteInventoryItem[];
35
36
  }
@@ -41,7 +42,10 @@ export interface ImportNextRouteInventoryOptions {
41
42
  includeApi?: boolean;
42
43
  limit?: number;
43
44
  createScenarios?: boolean;
45
+ createActionScenarios?: boolean;
44
46
  createWorkflows?: boolean;
47
+ createActionWorkflows?: boolean;
48
+ actionWorkflowGrouping?: "route" | "area-kind";
45
49
  workflowTarget?: "local" | "sandbox";
46
50
  workflowProvider?: string;
47
51
  workflowExecution?: Partial<WorkflowExecutionInput>;
@@ -52,6 +56,7 @@ export interface ImportNextRouteInventoryResult {
52
56
  updated: number;
53
57
  deduped: number;
54
58
  scenarios: Scenario[];
59
+ actionScenarios: Scenario[];
55
60
  workflows: TestingWorkflow[];
56
61
  }
57
62
  export declare function discoverNextRouteInventory(options: {
@@ -62,5 +67,6 @@ export declare function discoverNextRouteInventory(options: {
62
67
  limit?: number;
63
68
  }): NextRouteInventory;
64
69
  export declare function scenarioInputForNextRoute(item: NextRouteInventoryItem, projectId?: string): CreateScenarioInput;
70
+ export declare function scenarioInputsForNextRouteActions(item: NextRouteInventoryItem, projectId?: string): CreateScenarioInput[];
65
71
  export declare function importNextRouteInventory(options: ImportNextRouteInventoryOptions): ImportNextRouteInventoryResult;
66
72
  //# sourceMappingURL=next-route-inventory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"next-route-inventory.d.ts","sourceRoot":"","sources":["../../src/lib/next-route-inventory.ts"],"names":[],"mappings":"AAKA,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,CA8DrB;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,+BAA+B,GACvC,8BAA8B,CAuBhC"}
1
+ {"version":3,"file":"next-route-inventory.d.ts","sourceRoot":"","sources":["../../src/lib/next-route-inventory.ts"],"names":[],"mappings":"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"}
@@ -26,11 +26,37 @@ export interface WorkflowFanoutResult {
26
26
  passed: number;
27
27
  failed: number;
28
28
  items: WorkflowFanoutItem[];
29
+ preflight?: WorkflowFanoutPreflightResult;
29
30
  }
30
31
  export interface WorkflowFanoutDependencies extends WorkflowRunnerDependencies {
31
32
  runTestingWorkflow?: typeof runTestingWorkflow;
33
+ preflight?: (workflows: TestingWorkflow[]) => WorkflowFanoutPreflightResult | Promise<WorkflowFanoutPreflightResult>;
34
+ providerApiKeyResolver?: (provider: string, env: Record<string, string | undefined>) => string | undefined | Promise<string | undefined>;
35
+ commandExists?: (command: string) => boolean;
36
+ credentialResolver?: (value: string) => string | null;
37
+ env?: Record<string, string | undefined>;
38
+ }
39
+ export interface WorkflowFanoutPreflightCheck {
40
+ name: string;
41
+ ok: boolean;
42
+ required: boolean;
43
+ message: string;
44
+ workflows?: string[];
45
+ details?: Record<string, unknown>;
46
+ }
47
+ export interface WorkflowFanoutPreflightResult {
48
+ ok: boolean;
49
+ checks: WorkflowFanoutPreflightCheck[];
50
+ }
51
+ interface WorkflowFanoutPreflightDependencies {
52
+ providerApiKeyResolver?: WorkflowFanoutDependencies["providerApiKeyResolver"];
53
+ commandExists?: WorkflowFanoutDependencies["commandExists"];
54
+ credentialResolver?: WorkflowFanoutDependencies["credentialResolver"];
55
+ env?: Record<string, string | undefined>;
32
56
  }
33
57
  export declare function normalizeFanoutWorkerCount(value: number | undefined): number;
34
58
  export declare function resolveWorkflowFanoutSelection(options: Pick<WorkflowFanoutOptions, "workflowIds" | "projectId" | "tags" | "includeDisabled">): TestingWorkflow[];
59
+ export declare function checkWorkflowFanoutReadiness(workflows: TestingWorkflow[], dependencies?: WorkflowFanoutPreflightDependencies): Promise<WorkflowFanoutPreflightResult>;
35
60
  export declare function runWorkflowFanout(options: WorkflowFanoutOptions, dependencies?: WorkflowFanoutDependencies): Promise<WorkflowFanoutResult>;
61
+ export {};
36
62
  //# sourceMappingURL=workflow-fanout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workflow-fanout.d.ts","sourceRoot":"","sources":["../../src/lib/workflow-fanout.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,KAAK,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AACpH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB;IAC/D,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,kBAAkB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA2B,SAAQ,0BAA0B;IAC5E,kBAAkB,CAAC,EAAE,OAAO,kBAAkB,CAAC;CAChD;AASD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAM5E;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,IAAI,CAAC,qBAAqB,EAAE,aAAa,GAAG,WAAW,GAAG,MAAM,GAAG,iBAAiB,CAAC,GAAG,eAAe,EAAE,CA8BhK;AAuBD,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,qBAAqB,EAC9B,YAAY,GAAE,0BAA+B,GAC5C,OAAO,CAAC,oBAAoB,CAAC,CAyD/B"}
1
+ {"version":3,"file":"workflow-fanout.d.ts","sourceRoot":"","sources":["../../src/lib/workflow-fanout.ts"],"names":[],"mappings":"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"}
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.51",
55
+ version: "0.0.53",
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",
@@ -47090,7 +47090,7 @@ import { join as join14 } from "path";
47090
47090
  // package.json
47091
47091
  var package_default = {
47092
47092
  name: "@hasna/testers",
47093
- version: "0.0.51",
47093
+ version: "0.0.53",
47094
47094
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
47095
47095
  type: "module",
47096
47096
  main: "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/testers",
3
- "version": "0.0.51",
3
+ "version": "0.0.53",
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",