# APITestGen Scenario YAML Guide

[`scenario_testcases/`](./scenario_testcases/) stores reusable scenario YAML
files for the OpenAPI scenario E2E framework in
[`tidbcloud/tidb-management-service`](https://github.com/tidbcloud/tidb-management-service).

The Go runner entry point is:

[`functional-test/cases/openapidedicated/autogencase/Scenario_suite_test.go`](https://github.com/tidbcloud/tidb-management-service/blob/main/functional-test/cases/openapidedicated/autogencase/Scenario_suite_test.go)

The GitHub Actions workflows copy YAML files from this repository into:

[`functional-test/cases/openapidedicated/autogencase/testdata`](https://github.com/tidbcloud/tidb-management-service/tree/main/functional-test/cases/openapidedicated/autogencase/testdata)

before running:

```sh
TIDB_MANAGEMENT_SERVICE_DIR=/path/to/tidb-management-service
cd "${TIDB_MANAGEMENT_SERVICE_DIR}/functional-test"
make GO_TEST_RUN_ARGS='TestScenario' GO_TEST_TESTIFY_METHOD='.*' test-openapidedicated-autogencase
```

## Repository Map

- `scenario_testcases/`: committed scenario YAML files consumed by GHA.
- `skills/`: repository-specific agent workflows for scenario authoring, review,
  and framework exploration.
- `llm-scenario-generator/`: LLM-based exploration and raw scenario generation.
- `log-to-reusable/`: converts exploration logs into reusable YAML steps.
- `.github/workflows/nl2scenario.yaml`: manually generates reusable steps from a natural-language task.

Important runner-side files:

- [`Scenario_suite_test.go`](https://github.com/tidbcloud/tidb-management-service/blob/main/functional-test/cases/openapidedicated/autogencase/Scenario_suite_test.go)
- [`model/generationsteps.go`](https://github.com/tidbcloud/tidb-management-service/blob/main/functional-test/cases/openapidedicated/autogencase/model/generationsteps.go)
- [`actionbuilder.go`](https://github.com/tidbcloud/tidb-management-service/blob/main/functional-test/cases/openapidedicated/autogencase/actionbuilder.go)
- [`action.go`](https://github.com/tidbcloud/tidb-management-service/blob/main/functional-test/cases/openapidedicated/autogencase/action.go)
- [`functional-test/e2e-commands.mk`](https://github.com/tidbcloud/tidb-management-service/blob/main/functional-test/e2e-commands.mk)
- [`e2e-openapi-dedicated-scenario.yaml`](https://github.com/tidbcloud/tidb-management-service/blob/main/.github/workflows/e2e-openapi-dedicated-scenario.yaml)
- [`e2e-openapi-byoc-scenario.yaml`](https://github.com/tidbcloud/tidb-management-service/blob/main/.github/workflows/e2e-openapi-byoc-scenario.yaml)
- [`e2e-openapi-essential-v2-scenario.yaml`](https://github.com/tidbcloud/tidb-management-service/blob/main/.github/workflows/e2e-openapi-essential-v2-scenario.yaml)

## Agent Skills

Use the repository-local workflows under [`skills/`](./skills/) as the preferred
entry point for agent-assisted work. They are repository guidance files loaded
explicitly from their checked-in paths. Before starting a task, inspect
[`skills/`](./skills/) and load the matching `SKILL.md`. Use these skills for:

- authoring or updating scenario YAML test cases;
- reviewing changed test cases for correctness, portability, security,
  resource cleanup, assertions, retries, placeholders, and runner compatibility;
- learning the TiDB Cloud E2E framework, including the YAML schema, execution
  flow, runner behavior, GitHub Actions integration, and troubleshooting path.

The bundled
[`review-scenario-yaml`](./skills/review-scenario-yaml/SKILL.md) skill performs
read-only review of changed files under `scenario_testcases/`. Load it from the
linked path. It does not edit test cases unless the user explicitly requests
fixes. The
[`write-scenario-yaml`](./skills/write-scenario-yaml/SKILL.md) skill creates or
updates scenario cases only after establishing explicit evidence for every API
or command call, then requires `review-scenario-yaml` and repairs supported
findings. Load it from the linked path. Inspect [`skills/`](./skills/) for
additional task-specific guidance as it is added.

## Quick Start

1. Add or edit a YAML file under `scenario_testcases/`.
2. Use an existing file with the same API tier/provider as the template.
3. Keep every `request`, `expect`, and `save` block as valid JSON embedded in YAML.
4. Use placeholders instead of secrets or environment-specific IDs.
5. Run the matching scenario workflow in `tidb-management-service` only with explicit authority and protected credentials.

**LIVE / RESOURCE-MUTATING:** local replay executes scenario HTTP and command
steps and can create, change, or delete TiDB Cloud and provider resources. Use
only an authorized disposable test environment. Copy the YAML into the embedded
Go testdata directory before running `go test`, because the runner uses
`go:embed *.yaml`.

Clone the repositories from
[`tidbcloud/APITestGen`](https://github.com/tidbcloud/APITestGen) and
[`tidbcloud/tidb-management-service`](https://github.com/tidbcloud/tidb-management-service),
then set each directory independently. The repositories do not need to share a
parent directory.

```sh
APITESTGEN_DIR=/path/to/APITestGen
TIDB_MANAGEMENT_SERVICE_DIR=/path/to/tidb-management-service

: "${TIDB_CLOUD_API_KEY:?load from an approved secret source}"
: "${API_TESTER_DSN:?load from an approved secret source}"
: "${API_GENERATOR_DSN:?load from an approved secret source}"

if [ -n "${TIDB_CLOUD_ACCOUNT_TOKEN:-}" ]; then
  export OPEN_API_DEDICATED_ACCOUNT_TOKEN="${TIDB_CLOUD_ACCOUNT_TOKEN}"
else
  : "${TIDB_CLOUD_LOGIN_URL:?load from an approved secret source}"
  : "${TIDB_CLOUD_ACCOUNT_EMAIL:?load from an approved secret source}"
  : "${TIDB_CLOUD_ACCOUNT_PWD:?load from an approved secret source}"
  export OPEN_API_DEDICATED_LOGIN_URL="${TIDB_CLOUD_LOGIN_URL}"
  export OPEN_API_DEDICATED_ACCOUNT_EMAIL="${TIDB_CLOUD_ACCOUNT_EMAIL}"
  export OPEN_API_DEDICATED_ACCOUNT_PWD="${TIDB_CLOUD_ACCOUNT_PWD}"
fi

cp "${APITESTGEN_DIR}/scenario_testcases/essential_v2_cluster_crud_steps.yaml" \
  "${TIDB_MANAGEMENT_SERVICE_DIR}/functional-test/cases/openapidedicated/autogencase/testdata/"
cd "${TIDB_MANAGEMENT_SERVICE_DIR}/functional-test"

OPEN_API_DEDICATED_ENV='<environment>' \
OPEN_API_DEDICATED_HOST='<api-host-without-scheme>' \
OPEN_API_DEDICATED_API_KEY="${TIDB_CLOUD_API_KEY}" \
OPEN_API_DEDICATED_PROJECT_ID='<project_id>' \
OPEN_API_DEDICATED_TENANT_ID='<tenant_id>' \
OPEN_API_DEDICATED_CLOUD_PROVIDER='<provider>' \
OPEN_API_DEDICATED_REGION='<region>' \
OPEN_API_DEDICATED_DB_API_TESTER_DSN="${API_TESTER_DSN}" \
OPEN_API_DEDICATED_DB_API_GENERATOR_DSN="${API_GENERATOR_DSN}" \
OPEN_API_DEDICATED_READ_CASE_FROM_FS=true \
OPEN_API_DEDICATED_SCENARIO_YAML=essential_v2_cluster_crud_steps.yaml \
OPEN_API_DEDICATED_SAVE_RESULT_TO_DB=false \
OPEN_API_DEDICATED_SKIP_TEAR_DOWN=false \
make GO_TEST_RUN_ARGS='TestScenario' GO_TEST_TESTIFY_METHOD='.*' test-openapidedicated-autogencase
```

Load `TIDB_CLOUD_ACCOUNT_TOKEN` from a protected source. If no account token is
available, load all three protected fallback values:
`TIDB_CLOUD_LOGIN_URL`, `TIDB_CLOUD_ACCOUNT_EMAIL`, and
`TIDB_CLOUD_ACCOUNT_PWD`. The current runner attempts browser login when the
account token is empty. Keep all API keys, account credentials, and DSNs in an
approved secret manager or protected process environment, never in this
repository or shell history.

Do not commit real API keys, tokens, DSNs, cloud credentials, account passwords,
or SAS tokens.

## Execution Flow

1. A repaired workflow dispatch receives only non-secret environment, provider,
   region, tenant/project, config, and comma-separated YAML selectors; the
   workflow reads credentials from GitHub repository or environment secrets.
2. The workflow builds a matrix, one job per YAML file.
3. It installs runtime tools needed by command steps, such as `mysqlsh`, Chrome,
   AWS CLI, Azure CLI, or Aliyun CLI.
4. It checks out `tidb-management-service`.
5. If `fetch_from_apitestgen_repo` is true, it fetches or checks out
   `APITestGen`, then copies the selected YAML into
   `functional-test/cases/openapidedicated/autogencase/testdata`.
6. `TestMain` loads config from environment variables with the
   `OPEN_API_DEDICATED_` prefix.
7. `TestScenario` initializes placeholders such as `project_id`, `tenant_id`,
   `region`, `region_id`, `cloud_provider`, and `open_api_key`.
8. `SetupSuite` loads YAML from embedded testdata when `read_case_from_fs` is
   true. Otherwise it loads generation steps from the generator DB by
   `generation_id`.
9. `SetupSuite` validates that every step's `request`, `expect`, and non-null
   `save` JSON can be parsed into the runner's object shapes.
10. `Test_001_ExecuteActions` executes steps in YAML order, builds requests or
    commands, evaluates expectations, saves placeholders, and appends results.
11. `Test_999_Summary` writes result records when `save_result_to_db` is true,
    and the workflow summarizes `e2e-tests.xml`.
12. `TearDownSuite` deletes created clusters unless `skip_tear_down` is true.

## YAML Structure

Each file has two top-level keys:

```yaml
scenario:
  id: 2c8b5b1e-6cdc-4d57-8236-35dae49b490c
  context:
    base_path: /v1beta2
    host: null
  description: create_essential_v2_cluster
  name: create_essential_v2_cluster
steps:
  - operation_id: TidbService_CreateTidb
    request_type: http
    request: |-
      {}
    expect: |-
      {}
    save: null
    max_retries: 0
    delay_after: 0
    continue_on_unexpected: false
    order: 0
```

### `scenario`

| Field | Meaning |
|---|---|
| `id` | Stable scenario ID. Usually a UUID. |
| `name` | Human-readable scenario name. |
| `description` | Scenario purpose. |
| `context.base_path` | Prepended to relative HTTP paths, for example `/v1beta2`. |
| `context.host` | Optional host override. If empty, the runner uses `OPEN_API_DEDICATED_HOST`. |

If `context.host` contains `.env.`, the runner substitutes it using
`OPEN_API_DEDICATED_ENV`. For example, `.env.` becomes `.dev.`, `.staging.`, or
empty for prod depending on the configured environment.

### `steps[]`

| Field | Required | Meaning |
|---|---:|---|
| `operation_id` | Yes | API operation or command label. The result service name is the text before `_`. |
| `request_type` | Yes | `http`, `cmd_aws`, `cmd_gcp`, `cmd_azure`, `cmd_mysqlsh`, or `cmd_aliyun`. |
| `request` | Yes | JSON string parsed according to `request_type`. |
| `expect` | Yes | JSON string containing status/exit expectation and optional CEL. |
| `save` | No | JSON string that stores values into placeholders. Use `null` when unused. |
| `max_retries` | Yes | Number of retries after the first attempt. Total attempts are `max_retries + 1`. |
| `delay_after` | Yes | Retry delay in seconds. |
| `continue_on_unexpected` | Yes | When true, the suite logs the action error and continues. |
| `order` | Yes | Execution ordering value. Decimals are supported for inserted steps. |
| `case_id` | No | Case identifier for reporting. Generated files usually include it. |
| `case_type` | No | Informational case category. |
| `step_type` | No | Informational source such as `generated` or `defined`. |
| `stage` | No | Informational stage such as `prepare`, `execute`, or `teardown`. |
| `meta` | No | JSON metadata string. Use `"{}"` if there is no metadata. |
| `generation_id` | No | Scenario generation ID for DB-backed flows and reporting. |
| `name` | No | Human-readable step name. |
| `description` | No | Step purpose. |

`request_type` constants for SDK requests exist in code, but `TestScenario`
currently builds only HTTP and command actions.

## HTTP Request JSON

For `request_type: http`, `request` must be a JSON object:

```json
{
  "auth": {
    "auth_type": "digest",
    "content": {
      "public_key": "placeholder",
      "private_key": "placeholder"
    }
  },
  "method": "POST",
  "path": "/tidbs",
  "path_params": {},
  "query_params": {},
  "headers": {},
  "body": {
    "displayName": "sce-essential-v2-cluster-crud",
    "regionId": "{regionId}",
    "minRcu": "5000",
    "maxRcu": "6000",
    "servicePlan": "Essential_V2",
    "cloudProvider": "{cloud_provider}"
  },
  "body_params": {
    "regionId": "region_id",
    "cloud_provider": "cloud_provider"
  }
}
```

| Field | Meaning |
|---|---|
| `auth.auth_type` | `digest`, `bearer_token`, or `customized`. |
| `auth.content` | Digest credential object. `placeholder` means use `OPEN_API_DEDICATED_API_KEY`. |
| `auth.auth_role` | Placeholder key for an alternate digest API key, commonly `resource_apikey` or `target_apikey`. |
| `method` | HTTP method. The runner uppercases it. |
| `path` | Relative path or absolute URL. Relative paths become `https://{host}{base_path}{path}`. |
| `path_params` | Replaces `{name}` in `path`; values are placeholder keys. |
| `query_params` | Query object. Strings can resolve from placeholders, arrays repeat the query key. |
| `headers` | Static headers. |
| `body` | JSON request body. |
| `body_params` | Maps body placeholder names to scenario placeholder keys. |

Auth behavior:

- `digest` with `content.public_key/private_key = "placeholder"` uses
  `OPEN_API_DEDICATED_API_KEY`.
- `digest` with `auth_role` uses a value from `OPEN_API_DEDICATED_AUTH_ROLE`,
  which is a JSON map copied into placeholders.
- `bearer_token` sets `Authorization: Bearer <token>`, where the token comes
  from `OPEN_API_DEDICATED_ACCOUNT_TOKEN` or browser login.
- `customized` adds `X-Auth-Content` from the OpenAPI key unless that header is
  already present. Use it for regional/shared-pool style cases.

When `body.displayName` is present, the runner appends a timestamp suffix. This
keeps cluster names unique across retries and parallel GHA jobs.

## Command Request JSON

For command steps, `request` must include `name`, `args`, and optionally
`args_params`.

```json
{
  "name": "aws",
  "args": [
    "ec2",
    "describe-vpcs",
    "--region",
    "{region}",
    "--filters",
    "Name=isDefault,Values=true",
    "--output",
    "json"
  ],
  "args_params": {
    "region": "region"
  }
}
```

Supported mappings:

| `request_type` | Required `name` |
|---|---|
| `cmd_aws` | `aws` |
| `cmd_gcp` | `gcloud` |
| `cmd_azure` | `az` |
| `cmd_mysqlsh` | `mysqlsh` |
| `cmd_aliyun` | `aliyun` |

Placeholders in command args are replaced only for args that do not start with
`--`. Put placeholder values in separate args after flags, as shown above.

## Expectations

HTTP expectations:

```json
{
  "status_code": 200,
  "cel": "body.state == 'ACTIVE'"
}
```

Command expectations:

```json
{
  "exit_code": 0,
  "cel": "output.size() > 0"
}
```

Notes:

- HTTP CEL receives `body`, parsed from the JSON response body.
- Command CEL receives `output`. If command output is JSON, it is parsed as a
  map or list. Otherwise `output` is a string.
- Some older command YAMLs use `status_code`; the current Go struct expects
  `exit_code`. A non-zero command still fails before CEL evaluation because
  `exec.Command(...).CombinedOutput()` returns an error.
- Empty `cel` means only the status code or command execution is checked.

## Save Placeholders

Use `save.placeholder` to store values for later steps:

```json
{
  "placeholder": [
    {
      "key": "cluster_1",
      "eval": "body.tidbId"
    }
  ]
}
```

HTTP save expressions receive `body` and `saved_placeholder`. Command save
expressions receive `output`; the code also exposes `saved_palceholder` with
that typo for current-action saved values.

Common placeholders initialized by the suite:

| Placeholder | Source |
|---|---|
| `open_api_key` | `OPEN_API_DEDICATED_API_KEY` |
| `project_id` | `OPEN_API_DEDICATED_PROJECT_ID` |
| `org_id`, `tenant_id` | `OPEN_API_DEDICATED_TENANT_ID` |
| `region` | `OPEN_API_DEDICATED_REGION`, or provider default |
| `region_id` | `{cloud_provider}-{region}` |
| `cloud_provider` | `OPEN_API_DEDICATED_CLOUD_PROVIDER` |
| `iam_aws_key_id`, `iam_aws_key_secret` | `IAM_AWS_KEY_ID`, `IAM_AWS_KEY_SECRET` |
| `iam_ali_key_id`, `iam_ali_key_secret` | `IAM_ALI_KEY_ID`, `IAM_ALI_KEY_SECRET` |
| `azure_account_sas_token` | `OPEN_API_DEDICATED_AZURE_ACCOUNT_SAS_TOKEN` |
| `azure_blob_sas_token` | `OPEN_API_DEDICATED_AZURE_BLOB_SAS_TOKEN` |
| `cluster_id` | `OPEN_API_DEDICATED_GEN_CLUSTER_ID`, when set |
| `sharedpool_id` | `OPEN_API_DEDICATED_GEN_SHAREDPOOL_ID`, when set |
| `curr_action_do_start_unix` | Set by each action before execution |
| `curr_action_delay_10min_time` | Current UTC time plus 10 minutes |
| `curr_action_delay_10min_hhmm` | Current UTC time plus 10 minutes formatted as `HH:mm` |

For cluster IDs, prefer keys such as `cluster_1`, `cluster_2`. The teardown code
attempts to delete every placeholder whose key starts with `cluster`, so avoid
using the `cluster...` prefix for non-cluster IDs unless cleanup warnings are
acceptable.

## Placeholder Rules

Path params:

```json
{
  "path": "/tidbs/{tidbId}",
  "path_params": {
    "tidbId": "cluster_1"
  }
}
```

This replaces `{tidbId}` with the current `cluster_1` placeholder value.

Body params:

```json
{
  "body": {
    "regionId": "{regionId}",
    "nodeCount": "{node_count:integer}",
    "displayName": "restore-{cluster_id}"
  },
  "body_params": {
    "regionId": "region_id",
    "node_count": "target_node_count",
    "cluster_id": "cluster_1"
  }
}
```

Typed placeholders are supported only when the whole string is a single
placeholder:

- `{name:integer}` parses the value as an integer.
- `{name:number}` parses the value as a float or integer.

Expectation CEL can also refer to placeholders with `{placeholder_key}`. The
runner replaces those before compiling CEL.

## Examples

### Create A Cluster And Save Its ID

```yaml
- operation_id: TidbService_CreateTidb
  request_type: http
  request: |-
    {
      "method": "POST",
      "path": "/tidbs",
      "headers": {},
      "query_params": {},
      "body": {
        "displayName": "sce-example",
        "regionId": "{regionId}",
        "minRcu": "5000",
        "maxRcu": "6000",
        "servicePlan": "Essential_V2",
        "cloudProvider": "{cloud_provider}"
      },
      "body_params": {
        "regionId": "region_id",
        "cloud_provider": "cloud_provider"
      },
      "auth": {
        "auth_type": "digest",
        "content": {
          "public_key": "placeholder",
          "private_key": "placeholder"
        }
      }
    }
  expect: |-
    {
      "status_code": 200,
      "cel": ""
    }
  save: |-
    {
      "placeholder": [
        {
          "key": "cluster_1",
          "eval": "body.tidbId"
        }
      ]
    }
  max_retries: 0
  delay_after: 0
  continue_on_unexpected: false
  order: 0
```

### Poll Until Active

```yaml
- operation_id: TidbService_GetTidb
  request_type: http
  request: |-
    {
      "method": "GET",
      "path": "/tidbs/{tidbId}",
      "path_params": {
        "tidbId": "cluster_1"
      },
      "query_params": {},
      "headers": {},
      "body": {},
      "auth": {
        "auth_type": "digest",
        "content": {
          "public_key": "placeholder",
          "private_key": "placeholder"
        }
      }
    }
  expect: |-
    {
      "status_code": 200,
      "cel": "body.state == 'ACTIVE'"
    }
  save: null
  max_retries: 120
  delay_after: 30
  continue_on_unexpected: false
  order: 1
```

### Run SQL With `mysqlsh`

The following block demonstrates command placeholder binding; it is not a
standalone runnable step.

```yaml
- operation_id: ExecuteSQL
  request_type: cmd_mysqlsh
  request: |-
    {
      "name": "mysqlsh",
      "args": [
        "mysql://{cluster_userprefix}.root@{cluster_endpoint}:{cluster_port}/test",
        "--sql",
        "-e",
        "SELECT 1 AS ok;",
        "--result-format",
        "json",
        "--quiet-start",
        "2"
      ],
      "args_params": {
        "cluster_userprefix": "cluster_userprefix",
        "cluster_endpoint": "cluster_endpoint",
        "cluster_port": "cluster_port"
      }
    }
  expect: |-
    {
      "exit_code": 0,
      "cel": "output.ok == 1"
    }
  save: null
  max_retries: 0
  delay_after: 0
  continue_on_unexpected: false
  order: 10
```

The example intentionally omits database credentials. The runner does not
import arbitrary workflow environment variables into the placeholder map, and
saved placeholders appear in debug logs. Before making the step runnable,
configure a reviewed non-interactive `mysqlsh` credential mechanism outside the
scenario YAML. Never put a database password in YAML or a saved placeholder.

### Use Alternate RBAC API Keys

On a repaired workflow contract, store owner, resource-role, and target-role API
keys in GitHub repository or environment secrets. The workflow maps those
secrets directly to `OPEN_API_DEDICATED_API_KEY` and the
`OPEN_API_DEDICATED_AUTH_ROLE` JSON environment value; it must not carry them
through `workflow_dispatch` inputs. A YAML request can then select the role by
its non-secret placeholder name:

```json
{
  "auth": {
    "auth_type": "digest",
    "auth_role": "target_apikey"
  }
}
```

## GitHub Actions Inputs

Common scenario workflows:

- `e2e-openapi-dedicated-scenario.yaml`: Dedicated API scenarios.
- `e2e-openapi-byoc-scenario.yaml`: BYOC scenarios.
- `e2e-openapi-essential-v2-scenario.yaml`: Essential V2 scenarios.

Important inputs and derived environment variables:

| Workflow input/config | Runner variable |
|---|---|
| `environment` | `OPEN_API_DEDICATED_ENV` |
| `cloud_provider` | `OPEN_API_DEDICATED_CLOUD_PROVIDER` |
| `region` | `OPEN_API_DEDICATED_REGION` |
| `project_id` | `OPEN_API_DEDICATED_PROJECT_ID` |
| `tenant_id` | `OPEN_API_DEDICATED_TENANT_ID` |
| selected `scenario_yamls_*` value | `OPEN_API_DEDICATED_SCENARIO_YAML` |
| GitHub API-key secret | `OPEN_API_DEDICATED_API_KEY` |
| GitHub alternate-role secret map | `OPEN_API_DEDICATED_AUTH_ROLE` |
| `config.fetch_from_apitestgen_repo` | `OPEN_API_DEDICATED_READ_CASE_FROM_FS` |
| `config.save_result_to_db` | `OPEN_API_DEDICATED_SAVE_RESULT_TO_DB` |
| Azure SAS config | Azure SAS placeholder variables |

Dedicated and BYOC workflows fetch APITestGen `main` by default. Essential V2
also supports `apitestgen_branch`, so use it when testing a YAML PR before merge.

At the runner revision inspected on 2026-07-15, some workflow contracts still
accept credential-bearing `auth` strings through `workflow_dispatch`; those
contracts are incompatible with the protected-secret guidance above. Do not use
them as runnable examples. Repair the selected workflow to read GitHub
repository or environment secrets before dispatching non-secret inputs.

## Troubleshooting

Start from the failing workflow matrix job for the specific YAML file.

### YAML Was Not Fetched

Check the `Prepare matrix` and `Fetch scenario cases yaml files` steps.

- Ensure the workflow input references the exact filename under
  `APITestGen/scenario_testcases`.
- Ensure `config.fetch_from_apitestgen_repo` is true when the workflow should
  fetch from APITestGen.
- For Essential V2, ensure `apitestgen_branch` points to the branch containing
  the YAML.
- Check the `cat ${TESTDATA_DIR}/${YAML_FILE}` output in the fetch step.

### YAML Parse Failure

Failures during `SetupSuite` usually include messages such as
`ParseHTTPRequestSpec failed`, `ParseHTTPResponseExpect failed`, or
`ParseSaveSpec failed`.

Common causes:

- Invalid JSON inside the YAML block string, or a JSON array/scalar where
  `request`, `expect`, or non-null `save` requires an object.
- `request_type` does not match the request JSON shape.
- `expect` is missing, empty, or has invalid JSON.
- `save` has invalid or non-object JSON. Use `save: null` when no values need to
  be saved.

### Request URL Or Version Is Wrong

The URL is built as `https://{host}{base_path}{path}` unless `path` is already
absolute.

- Use `context.base_path: /v1beta2` with paths such as `/tidbs`.
- Use `context.base_path: null` when paths already include `/v1beta2`.
- Check `OPEN_API_DEDICATED_HOST` and `scenario.context.host`.
- For regional APIs, confirm whether the workflow host should be `cloud...`,
  `dedicated...`, or `regional-api...`.

### Placeholder Is Empty Or Wrong

Look for the runner log line:

`DEBUG: do step ... saved_placeholder:map[...]`

Then check:

- The producer step succeeded before saving placeholders.
- The `save.placeholder[].eval` path matches the response body.
- Later `path_params`, `body_params`, or `args_params` reference the saved key.
- `continue_on_unexpected: true` allows the suite to continue after an error,
  but the failed action will not save response placeholders.
- CEL placeholder replacement uses `{placeholder_key}` exactly.

### Expectation Fails

Look for:

- `DEBUG: http request, url:..., request_body:..., resp_header:..., resp_body:..., status_code:...`
- `response is unexpected, expression:...`
- `evalute expect failed`

Use `resp_body` to update the CEL expression. For backend investigation, copy
the `x-debug-trace-id` from `resp_header`.

### Command Step Fails

Check:

- The workflow installed the needed CLI for the selected provider.
- The provider login/OIDC step ran for the selected provider.
- Local runs have `aws`, `gcloud`, `az`, `aliyun`, or `mysqlsh` installed.
- Placeholder replacement is not placed inside an arg beginning with `--`.
- Command output is valid JSON when CEL expects a map/list.
- Use `exit_code`, not `status_code`, in new command expectations.

### Auth Or RBAC Fails

Check:

- `OPEN_API_DEDICATED_API_KEY` uses `public:private` format.
- `OPEN_API_DEDICATED_AUTH_ROLE` is valid JSON when `auth_role` is used.
- The YAML's `auth_role` key exists in the auth-role JSON.
- Headers such as `X-Auth-Method` match the API path being tested.
- For bearer-token cases, `OPEN_API_DEDICATED_ACCOUNT_TOKEN` is set or the
  workflow can log in with email/password and Chrome.

### Result Summary Looks Wrong

The workflow uses `test-summary/action` over `functional-test/e2e-tests.xml`.

- If the matrix job failed before `e2e-tests.xml` was produced, inspect the
  install/fetch/setup steps before the test step.
- If `save_result_to_db` is true, check the `scenario_result_records` table in
  the configured `api_tester` DSN.
- The Feishu notification summarizes the matrix outputs and links to the GHA
  run.

### Cleanup Failed

`TearDownSuite` deletes cluster IDs found in placeholders whose keys start with
`cluster`. Cleanup warnings usually include response headers and may include
`x-debug-trace-id`.

- Set `OPEN_API_DEDICATED_SKIP_TEAR_DOWN=true` only when intentionally keeping
  resources for debugging.
- If cleanup tries to delete a non-cluster value, rename that placeholder so it
  does not start with `cluster`.
- For AWS private endpoint tests, rejected VPC endpoints in the test VPC are
  also cleaned up.
