diff --git a/.agent/skills/ftr-testing/SKILL.md b/.agent/skills/ftr-testing/SKILL.md index f683065778dc7..54664d3e438de 100644 --- a/.agent/skills/ftr-testing/SKILL.md +++ b/.agent/skills/ftr-testing/SKILL.md @@ -12,6 +12,7 @@ FTR (FunctionalTestRunner) runs Kibana UI functional tests written in mocha with 1. Identify the FTR config and test file location. - FTR suites live under `test/**` or `x-pack/**/test/**` with config files. + - To confirm a config is actually executed in CI, check the relevant `.buildkite/ftr_*_configs.yml` and whether it’s listed under `enabled:` vs `disabled:`. 2. Understand the test structure. - Tests export a provider function that defines a mocha suite. - Use `describe/it/before/beforeEach/after/afterEach`. diff --git a/.agent/skills/scout-create-scaffold/SKILL.md b/.agent/skills/scout-create-scaffold/SKILL.md index 3c47705666be0..7d495f415ee67 100644 --- a/.agent/skills/scout-create-scaffold/SKILL.md +++ b/.agent/skills/scout-create-scaffold/SKILL.md @@ -1,123 +1,76 @@ --- name: Scout Create Scaffold -description: Use when creating, relocating, or reviewing Scout test scaffolds for a Kibana module and you must choose the correct test/scout path, ui/api split, or file naming conventions. +description: Generate or repair a Scout test scaffold for a Kibana plugin/package (test/scout*/{api,ui} Playwright configs, fixtures, example specs). Use when you need the initial Scout directory structure; prefer `node scripts/scout.js generate` with flags for non-interactive/LLM execution. --- -# Create Scout Scaffold +# Create Scout Scaffold (Generator-First) -## Overview +## Inputs to Collect -Place Scout tests next to the module under test using the repo's Scout path conventions. Core principle: derive the test root from the target module path first, then choose the ui/api split and naming rules. +- Module root (repo-relative path to the plugin/package directory, e.g. `x-pack/platform/plugins/shared/maps`) +- Test type: `api`, `ui`, or `both` +- Scout root directory name under `/test/` + - Default: `scout` (creates `/test/scout/...`) + - Custom config set: `scout_` (for example `scout_uiam_local`, `scout_cspm_agentless`) +- For UI scaffolds: whether tests can run in parallel (space-isolated). Default is parallel; use sequential when isolation is not possible. -## Core workflow +## Generate (Preferred) -1. Determine the module root. - - Walk up from the target file path until you find `kibana.jsonc` (preferred). - - If none, use the directory that represents the module boundary (plugin/package root). -2. Choose test type. - - UI test: uses Playwright page or page objects. - - API test: uses HTTP clients only (no browser). -3. Choose the Scout root. - - Default: `/test/scout`. - - If a custom Scout root exists (for example `test/scout_cspm_agentless`, `test/scout_uiam_local`), place new tests under the same root. -4. Prefer scaffolding via the Scout CLI when starting from scratch. - - `node scripts/scout.js generate` (also picks the correct Scout package import for the module) -5. Place files using Scout path conventions. -6. Create the minimal scaffold (see checklist below). +Run from the Kibana repo root: -## Quick reference +```bash +node scripts/scout.js generate --path --type +``` -| Purpose | Path pattern | -| -------------------- | ---------------------------------------------------------------------- | -| UI tests (sequential) | `test/scout{_}/ui/tests/**/*.spec.ts` | -| UI tests (parallel) | `test/scout{_}/ui/parallel_tests/**/*.spec.ts` | -| API tests (sequential) | `test/scout{_}/api/tests/**/*.spec.ts` | -| API tests (parallel) | `test/scout{_}/api/parallel_tests/**/*.spec.ts` | -| Global setup | `test/scout{_}/{ui,api}/{tests,parallel_tests}/global.setup.ts` | +Common variants: -## Path conventions +```bash +# UI scaffold, sequential (non-parallel) +node scripts/scout.js generate --path --type ui --no-ui-parallel -Scout test files must match: +# Generate into a custom Scout root (test/scout_/...) +node scripts/scout.js generate --path --type both --scout-root scout_ -UI: -``` -/test/scout{_}/ui/{tests,parallel_tests}/**/*.spec.ts -``` - -API: -``` -/test/scout{_}/api/{tests,parallel_tests}/**/*.spec.ts +# If some Scout directories already exist, generate only missing sections without prompting +node scripts/scout.js generate --path --type both --force ``` Notes: -- `{_}` is optional. Examples: `test/scout` (default), `test/scout_cspm_agentless`, `test/scout_uiam_local`. -- UI test files live in `ui/tests/` or `ui/parallel_tests/`. -- API test files live in `api/tests/` (sequential) or `api/parallel_tests/` (parallel). Parallel API runs are supported but require careful isolation (indices, saved objects, etc.). -- `global.setup.ts` lives under the configured `testDir` and uses worker-scoped fixtures only. -- Non-test helpers can live under `test/scout*/ui/helpers/` or `test/scout*/common/` (shared UI+API). - -## Scaffold checklist - -### UI tests - -- `test/scout*/ui/playwright.config.ts` - - If missing, copy from a similar Scout-enabled plugin in the same solution. - - Filename must be exactly `playwright.config.ts` (tooling discovery). -- Optional: `test/scout*/ui/parallel.playwright.config.ts` (plus `ui/parallel_tests/`) - - Filename must be exactly `parallel.playwright.config.ts` (tooling discovery). -- `test/scout*/ui/fixtures/index.ts` - - Provide the `test` fixture export used by UI tests. -- `test/scout*/ui/tests/` - - Place UI `.spec.ts` files here. -- Optional: `test/scout*/ui/parallel_tests/` -- Optional: `test/scout*/ui/tests/global.setup.ts` or `parallel_tests/global.setup.ts` -- Optional: `test/scout*/ui/tsconfig.json` (follow existing pattern in the module). - -### API tests - -- `test/scout*/api/playwright.config.ts` - - If missing, copy from a similar Scout-enabled plugin in the same solution. - - Filename must be exactly `playwright.config.ts` (tooling discovery). -- Optional: `test/scout*/api/parallel.playwright.config.ts` (plus `api/parallel_tests/`) - - Filename must be exactly `parallel.playwright.config.ts` (tooling discovery). -- `test/scout*/api/fixtures/index.ts` - - Provide the `apiTest` fixture export used by API tests. -- `test/scout*/api/tests/` - - Place API `.spec.ts` files here. -- Optional: `test/scout*/api/parallel_tests/` -- Optional: `test/scout*/api/tests/global.setup.ts` or `parallel_tests/global.setup.ts` -- Optional: `test/scout*/api/tsconfig.json` (follow existing pattern in the module). - -## Example - -Request: "Create a new scout test for x-pack/solutions/observability/plugins/apm/public/components/service_overview.tsx" - -Outcome: - -- Module root: `x-pack/solutions/observability/plugins/apm` -- UI tests path: `x-pack/solutions/observability/plugins/apm/test/scout/ui/tests/service_overview.spec.ts` -- Ensure `x-pack/solutions/observability/plugins/apm/test/scout/ui/playwright.config.ts` exists -- Ensure `x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/index.ts` exists - -## Common mistakes - -- Put test files directly under `test/scout*/ui` instead of `test/scout*/ui/tests`. -- Forget `{ tag: ... }` on UI suites/tests (Scout validates UI tags at runtime). API tests should also be tagged so CI/discovery can select the right deployment target. -- Use `.test.ts` or `.ts` instead of `.spec.ts` for Scout tests. -- Place UI tests under `api` (or API tests under `ui`). -- Create a new `test/scout_*` root when a module already uses `test/scout` (or vice versa). - -## Red flags - -- Unsure where the module root is. -- No `tests/` or `parallel_tests/` directory in the target path. -- Using `.test.ts` or `.ts` for a Scout test file. -- Creating a new `test/scout_*` root without a corresponding server config set or an existing precedent in the module. - -## Discovery / manifests - -- Scout config discovery and CI rely on `.meta` manifests under `/test/scout*/.meta/...`. -- After adding or moving Scout tests (or changing Playwright configs), run `node scripts/scout.js update-test-config-manifests` to refresh manifests. -- If you add a new custom Scout root like `test/scout_`, you typically also need a matching server config set under `src/platform/packages/shared/kbn-scout/src/servers/configs/custom/`. -- `run-tests` auto-detects the custom config dir from the Playwright config path; `start-server` requires `--config-dir `. +- The generator will not modify existing `test//{api,ui}` sub-directories. +- If any Scout directories already exist and you pass `--path`, you must also pass `--force` (otherwise the command fails rather than prompting). + +## What It Creates + +- API scaffold: + - `test//api/playwright.config.ts` + - `test//api/fixtures/constants.ts` + - `test//api/fixtures/index.ts` + - `test//api/tests/example.spec.ts` +- UI scaffold (sequential): + - `test//ui/playwright.config.ts` + - `test//ui/fixtures/constants.ts` + - `test//ui/fixtures/index.ts` + - `test//ui/fixtures/page_objects/*` + - `test//ui/tests/example.spec.ts` +- UI scaffold (parallel): + - `test//ui/parallel.playwright.config.ts` + - `test//ui/parallel_tests/example_one.spec.ts` + - `test//ui/parallel_tests/example_two.spec.ts` + - `test//ui/parallel_tests/global.setup.ts` + +## After Generating + +- Update `.meta` manifests when adding/moving configs or tests: + - `node scripts/scout.js update-test-config-manifests` +- CI plumbing: + - Add the plugin/package to `.buildkite/scout_ci_config.yml` (the generator warns about this). +- Custom server config sets: + - If you create/use `test/scout_`, you typically also need a matching server config under `src/platform/packages/shared/kbn-scout/src/servers/configs/custom/`. + - `start-server` requires `--config-dir ` when using a custom server config set. + +## Path Conventions (Specs) + +- UI sequential specs: `test/scout*/ui/tests/**/*.spec.ts` +- UI parallel specs: `test/scout*/ui/parallel_tests/**/*.spec.ts` +- API specs: `test/scout*/api/tests/**/*.spec.ts` diff --git a/.agent/skills/scout-migrate-from-ftr/references/migration-workflow.md b/.agent/skills/scout-migrate-from-ftr/references/migration-workflow.md index df1b34a9dd1c9..b4770278612ab 100644 --- a/.agent/skills/scout-migrate-from-ftr/references/migration-workflow.md +++ b/.agent/skills/scout-migrate-from-ftr/references/migration-workflow.md @@ -16,10 +16,34 @@ Use this as a checklist when migrating FTR tests to Scout. ## 3) Translate the test structure -- `describe/it` -> `test.describe/test` or `apiTest.describe/apiTest`. +- `describe/it` -> `test.describe/test` or `apiTest.describe/apiTest` (but don’t assume 1:1 `it` -> `test`). - `before/after` -> `test.beforeAll/test.afterAll`. - `beforeEach/afterEach` -> `test.beforeEach/test.afterEach`. +### `it` blocks are sometimes steps (not full test cases) + +In FTR it’s common for multiple `it(...)` blocks in one `describe(...)` to behave like a single user journey (shared browser state across `it`s). +In Scout (Playwright), each `test(...)` runs with a fresh browser context, so you usually can’t preserve that state across multiple `test`s. + +Guideline: + +- If the FTR suite uses multiple `it(...)` blocks as sequential steps of one flow, combine them into a single `test(...)` and convert the step boundaries into `test.step(...)`. +- If an `it(...)` block is already an independent test case, keep it as its own `test(...)` and ensure it sets up its own preconditions. + +Minimal sketch: + +```ts +// FTR: multiple `it`s continue in the same browser context +it('create entity', async () => {}); +it('edit entity', async () => {}); // continues... + +// Scout: combine into one test and use `test.step` for debuggability +test('create and edit entity', async () => { + await test.step('create entity', async () => {}); + await test.step('edit entity', async () => {}); +}); +``` + ## 4) Replace FTR dependencies - Replace FTR services with Scout fixtures (`pageObjects`, `browserAuth`, `apiClient`, `apiServices`, `kbnClient`, `esArchiver`, `requestAuth`, `samlAuth`). diff --git a/src/platform/packages/shared/kbn-scout/src/cli/generate.ts b/src/platform/packages/shared/kbn-scout/src/cli/generate.ts index 76b5470e1a9a8..615f6e10f5d72 100644 --- a/src/platform/packages/shared/kbn-scout/src/cli/generate.ts +++ b/src/platform/packages/shared/kbn-scout/src/cli/generate.ts @@ -8,6 +8,7 @@ */ import type { Command } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; import { REPO_ROOT } from '@kbn/repo-info'; import Fsp from 'fs/promises'; import inquirer from 'inquirer'; @@ -28,7 +29,30 @@ import { getScoutPackageImport, } from '../generator_content'; -type TestType = 'ui' | 'api' | 'both'; +const TEST_TYPES = ['ui', 'api', 'both'] as const; +type TestType = (typeof TEST_TYPES)[number]; + +function normalizeScoutRoot(scoutRootRaw: string): string { + const normalized = scoutRootRaw.trim().replace(/\\/g, '/'); + const stripped = normalized.startsWith('test/') ? normalized.slice('test/'.length) : normalized; + + if (!stripped) { + throw createFlagError(`--scout-root cannot be empty`); + } + + if (stripped.includes('/')) { + throw createFlagError(`--scout-root must be a directory name under "test/" (e.g. "scout")`); + } + + if (!/^scout(?:_[a-z0-9_]+)?$/.test(stripped)) { + throw createFlagError( + `--scout-root must match "scout" or "scout_", got "${stripped}"` + ); + } + + return stripped; +} + async function validatePath(input: string): Promise { const normalizedPath = input.trim(); if (!normalizedPath) { @@ -69,16 +93,15 @@ async function pathExists(targetPath: string): Promise { async function createDirectoryStructure( basePath: string, - testType: TestType, - uiParallel: boolean, - log: any + opts: { scoutRoot: string; generateApi: boolean; generateUi: boolean; uiParallel: boolean } ): Promise { const fullBasePath = Path.resolve(REPO_ROOT, basePath); + const scoutRootDir = Path.resolve(fullBasePath, 'test', opts.scoutRoot); const scoutPackage = getScoutPackageImport(basePath); const copyrightHeader = getCopyrightHeader(basePath); - if (testType === 'api' || testType === 'both') { - const apiTestDir = Path.resolve(fullBasePath, 'test/scout/api'); + if (opts.generateApi) { + const apiTestDir = Path.resolve(scoutRootDir, 'api'); const apiFixturesDir = Path.resolve(apiTestDir, 'fixtures'); const apiTestsDir = Path.resolve(apiTestDir, 'tests'); const apiConfigPath = Path.resolve(apiTestDir, 'playwright.config.ts'); @@ -104,8 +127,8 @@ async function createDirectoryStructure( await Fsp.writeFile(apiExampleSpecPath, apiSpecContent); } - if (testType === 'ui' || testType === 'both') { - const uiTestDir = Path.resolve(fullBasePath, 'test/scout/ui'); + if (opts.generateUi) { + const uiTestDir = Path.resolve(scoutRootDir, 'ui'); const uiFixturesDir = Path.resolve(uiTestDir, 'fixtures'); const uiPageObjectsDir = Path.resolve(uiFixturesDir, 'page_objects'); const uiPageObjectsIndexPath = Path.resolve(uiPageObjectsDir, 'index.ts'); @@ -113,7 +136,7 @@ async function createDirectoryStructure( const uiConstantsPath = Path.resolve(uiFixturesDir, 'constants.ts'); const uiFixturesIndexPath = Path.resolve(uiFixturesDir, 'index.ts'); - if (uiParallel) { + if (opts.uiParallel) { const uiParallelTestsDir = Path.resolve(uiTestDir, 'parallel_tests'); const uiConfigPath = Path.resolve(uiTestDir, 'parallel.playwright.config.ts'); const uiParallelSpecPathOne = Path.resolve(uiParallelTestsDir, 'example_one.spec.ts'); @@ -206,45 +229,88 @@ export const generateCmd: Command = { description: ` Generate Scout test directory structure for a plugin or package. - This command interactively prompts for: - - Relative path to plugin or package (e.g., x-pack/platform/plugins/shared/maps) - - Test type: ui, api, or both (default: api) - - For UI tests: whether tests can run in parallel (default: yes) + Interactive prompts are used by default. To run non-interactively, pass --path + (and optionally --type / --scout-root / --no-ui-parallel / --force). It creates the appropriate directory structure and Playwright config files. `, - flags: {}, - run: async ({ log }) => { - let relativePath: string = ''; + flags: { + string: ['path', 'type', 'scout-root'], + boolean: ['force', 'ui-parallel'], + alias: { + p: 'path', + t: 'type', + y: 'force', + }, + default: { + 'ui-parallel': true, + }, + help: ` + --path Relative path to the plugin or package (e.g. x-pack/platform/plugins/shared/maps) + --type Test type to generate: api | ui | both + --scout-root Directory name under /test/ (default: scout). Example: scout_uiam_local + --ui-parallel For UI scaffolds, generate parallel tests (default: true). Use --no-ui-parallel for sequential. + --force If some Scout directories already exist, generate only the missing sections without prompting + `, + examples: ` + node scripts/scout.js generate --path x-pack/platform/plugins/shared/maps --type api + node scripts/scout.js generate --path x-pack/platform/plugins/shared/maps --type ui --no-ui-parallel + node scripts/scout.js generate --path x-pack/platform/plugins/shared/security --type both --scout-root scout_uiam_local --force + `, + }, + run: async ({ flagsReader, log }) => { + const positionals = flagsReader.getPositionals(); + + const pathFromFlag = flagsReader.string('path'); + const pathFromPositional = positionals[0]; + if (pathFromFlag && pathFromPositional) { + throw createFlagError( + `Provide the path either as a positional argument or via --path, not both` + ); + } - while (true) { - const pathResult = await inquirer.prompt<{ path: string }>({ - type: 'input', - name: 'path', - message: - 'What is the relative path to the plugin or package? (e.g., x-pack/platform/plugins/shared/maps):', - validate: async (input) => { - const result = await validatePath(input); - if (result === true) { - return true; - } - return result as string; - }, - }); + const isNonInteractive = Boolean(pathFromFlag || pathFromPositional); + + let relativePath: string = ''; - relativePath = pathResult.path.trim(); + if (isNonInteractive) { + relativePath = (pathFromFlag ?? pathFromPositional)?.trim() ?? ''; const validationResult = await validatePath(relativePath); - if (validationResult === true) { - break; - } else { - log.error(validationResult as string); + if (validationResult !== true) { + throw createFlagError(validationResult as string); + } + } else { + while (true) { + const pathResult = await inquirer.prompt<{ path: string }>({ + type: 'input', + name: 'path', + message: + 'What is the relative path to the plugin or package? (e.g., x-pack/platform/plugins/shared/maps):', + validate: async (input) => { + const result = await validatePath(input); + if (result === true) { + return true; + } + return result as string; + }, + }); + + relativePath = pathResult.path.trim(); + const validationResult = await validatePath(relativePath); + if (validationResult === true) { + break; + } else { + log.error(validationResult as string); + } } } log.info(`Validated path: ${relativePath}`); const basePath = Path.resolve(REPO_ROOT, relativePath); - const scoutDir = Path.resolve(basePath, 'test/scout'); + const scoutRoot = normalizeScoutRoot(flagsReader.string('scout-root') ?? 'scout'); + + const scoutDir = Path.resolve(basePath, 'test', scoutRoot); const apiDir = Path.resolve(scoutDir, 'api'); const uiDir = Path.resolve(scoutDir, 'ui'); @@ -254,38 +320,51 @@ export const generateCmd: Command = { if (apiDirExists && uiDirExists) { log.warning( - 'Both test/scout/api and test/scout/ui already exist. The generator will not modify existing sub-directories.' + `Both test/${scoutRoot}/api and test/${scoutRoot}/ui already exist. The generator will not modify existing sub-directories.` ); return; } + const force = flagsReader.boolean('force'); if (scoutDirExists || apiDirExists || uiDirExists) { const existingDirs: string[] = []; if (apiDirExists) { - existingDirs.push('test/scout/api'); + existingDirs.push(`test/${scoutRoot}/api`); } if (uiDirExists) { - existingDirs.push('test/scout/ui'); + existingDirs.push(`test/${scoutRoot}/ui`); + } + if (existingDirs.length === 0 && scoutDirExists) { + existingDirs.push(`test/${scoutRoot}`); } log.warning( `Existing Scout test directories found: ${existingDirs.join( ', ' )}. The generator will not modify existing sub-directories.` ); - const continueResult = await inquirer.prompt<{ proceed: boolean }>({ - type: 'list', - name: 'proceed', - message: 'Do you want to continue and generate only missing sections?', - default: false, - choices: [ - { name: 'No', value: false }, - { name: 'Yes', value: true }, - ], - }); - if (!continueResult.proceed) { - log.info('Aborted.'); - return; + if (!force) { + if (isNonInteractive) { + throw createFlagError( + `Rerun with --force to generate only the missing sections under test/${scoutRoot}/` + ); + } + + const continueResult = await inquirer.prompt<{ proceed: boolean }>({ + type: 'list', + name: 'proceed', + message: 'Do you want to continue and generate only missing sections?', + default: false, + choices: [ + { name: 'No', value: false }, + { name: 'Yes', value: true }, + ], + }); + + if (!continueResult.proceed) { + log.info('Aborted.'); + return; + } } } @@ -297,52 +376,83 @@ export const generateCmd: Command = { return; } - const testTypeChoices: Array<{ name: string; value: TestType }> = []; - if (apiMissing) { - testTypeChoices.push({ name: 'API tests', value: 'api' }); - } - if (uiMissing) { - testTypeChoices.push({ name: 'UI tests', value: 'ui' }); - } - if (apiMissing && uiMissing) { - testTypeChoices.push({ name: 'Both API and UI tests', value: 'both' }); - } + const requestedType = flagsReader.enum('type', TEST_TYPES) as TestType | undefined; - const testTypeResult = await inquirer.prompt<{ testType: TestType }>({ - type: 'list', - name: 'testType', - message: 'What type of tests do you plan to add?', - default: apiMissing ? 'api' : 'ui', - choices: testTypeChoices, - }); + let testType: TestType; + if (requestedType) { + if (requestedType === 'api' && !apiMissing) { + throw createFlagError( + `test/${scoutRoot}/api already exists. The generator will not modify existing sub-directories.` + ); + } + if (requestedType === 'ui' && !uiMissing) { + throw createFlagError( + `test/${scoutRoot}/ui already exists. The generator will not modify existing sub-directories.` + ); + } + testType = requestedType; + } else if (isNonInteractive) { + testType = apiMissing ? 'api' : 'ui'; + } else { + const testTypeChoices: Array<{ name: string; value: TestType }> = []; + if (apiMissing) { + testTypeChoices.push({ name: 'API tests', value: 'api' }); + } + if (uiMissing) { + testTypeChoices.push({ name: 'UI tests', value: 'ui' }); + } + if (apiMissing && uiMissing) { + testTypeChoices.push({ name: 'Both API and UI tests', value: 'both' }); + } + + const testTypeResult = await inquirer.prompt<{ testType: TestType }>({ + type: 'list', + name: 'testType', + message: 'What type of tests do you plan to add?', + default: apiMissing ? 'api' : 'ui', + choices: testTypeChoices, + }); + testType = testTypeResult.testType; + } - const testType = testTypeResult.testType; log.info(`Selected test type: ${testType}`); + const shouldGenerateApi = apiMissing && (testType === 'api' || testType === 'both'); + const shouldGenerateUi = uiMissing && (testType === 'ui' || testType === 'both'); + let uiParallel = false; - if (testType === 'ui' || testType === 'both') { - const parallelResult = await inquirer.prompt<{ parallel: boolean }>({ - type: 'list', - name: 'parallel', - message: - 'Is it possible to design UI tests to run in parallel against the same cluster (e.g., in isolated Kibana spaces)?', - default: true, - choices: [ - { name: 'Yes', value: true }, - { name: 'No', value: false }, - ], - }); - uiParallel = parallelResult.parallel; + if (shouldGenerateUi) { + if (isNonInteractive) { + uiParallel = flagsReader.boolean('ui-parallel'); + } else { + const parallelResult = await inquirer.prompt<{ parallel: boolean }>({ + type: 'list', + name: 'parallel', + message: + 'Is it possible to design UI tests to run in parallel against the same cluster (e.g., in isolated Kibana spaces)?', + default: true, + choices: [ + { name: 'Yes', value: true }, + { name: 'No', value: false }, + ], + }); + uiParallel = parallelResult.parallel; + } log.info(`UI tests parallel: ${uiParallel}`); } log.info('Creating directory structure...'); - await createDirectoryStructure(relativePath, testType, uiParallel, log); + await createDirectoryStructure(relativePath, { + scoutRoot, + generateApi: shouldGenerateApi, + generateUi: shouldGenerateUi, + uiParallel, + }); log.success( `Successfully generated Scout test structure for ${Path.posix.join( relativePath.replace(/\\\\/g, '/'), - 'test/scout' + `test/${scoutRoot}` )}` ); log.write('\n');