# API de casos de ejecución

> Lista de casos en una ejecución, incluyendo estado del ciclo de vida en tiempo real y claves de sistema de opciones de campos personalizados.

`GET /api/v1/runs/{runUlid}/cases` devuelve todos los casos que pertenecen a
una ejecución abierta o cerrada. Se añadieron dos campos como
**extensiones aditivas y compatibles con versiones anteriores** — las
integraciones existentes que ignoran claves desconocidas no se ven afectadas.

## Listar casos de una ejecución

```
GET /api/v1/runs/{runUlid}/cases
```

Requiere `viewer`+ en el proyecto de la ejecución. Devuelve **404** si la
ejecución está fuera de la organización del llamador.

### Forma de la respuesta (por elemento)

Cada elemento del array `cases` incluye los campos habituales más:

| Campo                                 | Tipo             | Descripción                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `caseLifecycleStatus`                 | `object \| null` | El estado del ciclo de vida **actual** (en tiempo real, no del snapshot) del caso fuente. `null` cuando el campo de estado no está configurado o el caso fuente fue eliminado.                                                                                                                                                                                                                                                                                     |
| `customFieldValues[].optionSystemKey` | `string \| null` | La clave de sistema propia de la opción (p. ej. `status_active`, `status_draft`). `null` para opciones personalizadas definidas por el usuario y para tipos de campo no selectores.                                                                                                                                                                                                                                                                                |
| `executedByApiToken`                  | `object \| null` | El token de API que ejecutó el **intento actual** del caso (su último resultado en la ejecución): `{ ulid, name, app }`, donde `app` es el reporter de CI (`junit`, `playwright`, `jest`, `cypress`, `vitest`, `cucumber`) de un token de aplicación de la organización y `null` para un token de API personal. `null` cuando el caso no tiene resultados en la ejecución, cuando el intento actual se registró en la aplicación web y una vez eliminado el token. |
| `caseRevision`                        | `number \| null` | La revisión del caso fuente que este caso de la ejecución congeló: su título, campos y pasos son esa versión del caso. `null` solo en un caso de ejecución creado antes de que existieran las revisiones cuyo caso ya estaba eliminado.                                                                                                                                                                                                                            |
| `caseCurrentRevision`                 | `number \| null` | La revisión actual del caso fuente (`currentRevision` del caso de prueba). `null` cuando el caso fue eliminado.                                                                                                                                                                                                                                                                                                                                                    |
| `caseUpdated`                         | `boolean`        | `true` cuando la ejecución está abierta, el caso de la ejecución está pendiente (`untested`, o `retested` tras un reintento) y el caso cambió después de que este caso de la ejecución lo congelara (`caseCurrentRevision` > `caseRevision`). Siempre `false` para un caso de la ejecución completado y en una ejecución cerrada.                                                                                                                                  |
| `canApplyLatest`                      | `boolean`        | `true` cuando `caseUpdated` es `true` y el intento actual todavía no tiene resultados de pasos, de modo que [aplicar la última versión](#aplicar-la-última-versión-del-caso) puede actualizarlo.                                                                                                                                                                                                                                                                   |

#### `executedByApiToken` no es un responsable

Un reporter de CI escribe con un token de aplicación, que no es un miembro, así que un caso que
reporta conserva `assigneeUlid: null` salvo que se asigne a un miembro (por ejemplo, con
`assignFailedTo` del reporte). `executedByApiToken` solo indica qué token ejecutó el intento
actual, y un intento posterior registrado por un miembro lo reemplaza por `null`. La aplicación
web lo muestra en la columna de responsable de un caso sin miembro asignado; en una ejecución que
creó un token de aplicación (una ejecución de CI), un caso sin ninguno de los dos muestra allí el
token de la ejecución (su `author`). Ninguno es un responsable: el filtro por responsable, el bloqueo de resultados por
responsable, "asignados a mí" y la asignación por `PATCH` o masiva siguen viendo el caso sin
asignar.

#### Abrir un caso solo asigna un responsable vacío

`POST /api/v1/runs/{runUlid}/cases/{runCaseUlid}/open` asigna automáticamente a quien abre el caso
(o al responsable predeterminado de la ejecución) solo cuando `autoAssignOnOpen` del proyecto está
activo y el responsable del caso está vacío. No está vacío, y abrir el caso no asigna a nadie,
cuando:

- hay un miembro asignado: abrir nunca cambia a un miembro responsable;
- un token de aplicación de la organización, un reporter de CI, creó la ejecución (su
  `author.kind` es `api_token` y su `author.app` tiene valor): ningún caso de una ejecución de CI se
  asigna al abrirlo, ni siquiera después de que un miembro lo marque o lo reintente. Una ejecución
  que creó un token de API personal no es una ejecución de CI;
- en cualquier otra ejecución, un token de API ejecutó el intento actual del caso
  (`executedByApiToken` tiene valor). Cuando un miembro registra un intento más reciente, abrirlo
  vuelve a asignar.

Para el intento actual de un caso, los tokens de aplicación y los tokens de API personales cuentan
por igual. Un caso que se quita de una ejecución y se vuelve a agregar conserva sus resultados anteriores en ella, así que su intento
actual sigue siendo el más reciente de ellos.

#### Objeto `caseLifecycleStatus`

Cuando no es null, el objeto tiene la misma forma que un `CustomFieldValueEntry`:

| Campo             | Tipo             | Descripción                                                                                                   |
| ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------- |
| `optionUlid`      | `string`         | ULID de la opción de ciclo de vida seleccionada                                                               |
| `optionName`      | `string`         | Nombre visible de la opción, resuelto según el idioma de la solicitud (ver más abajo)                         |
| `optionColor`     | `string \| null` | Color hexadecimal, si existe                                                                                  |
| `optionIcon`      | `string \| null` | Identificador de icono, si existe                                                                             |
| `optionSystemKey` | `string \| null` | Clave de sistema: `status_active`, `status_draft`, `status_deprecated`, o `null` para opciones personalizadas |

**Idioma de la solicitud.** `optionName` se resuelve según el encabezado
`Accept-Language` de la solicitud: una organización que tradujo los nombres
de sus opciones de estado al español recibe el nombre en español al enviar
`Accept-Language: es`. Un idioma ausente o no soportado resuelve al nombre en
inglés, así que los integradores que nunca envían el encabezado obtienen
respuestas idénticas byte a byte a las de antes de este comportamiento.

### En tiempo real vs. snapshot

El **contenido** del caso de prueba (título, pasos, descripción) se
**congela en un snapshot** cuando el caso se añade a la ejecución — los cambios
en el caso original no afectan ninguna ejecución existente. `caseLifecycleStatus`
es deliberadamente **no** un snapshot: se resuelve en tiempo real mediante un
join al caso fuente, por lo que siempre refleja el estado actual del caso en el
repositorio. Si necesitas el estado del snapshot, usa el campo `priority`
(que sí forma parte del snapshot).

El caso de la ejecución registra qué revisión del caso congeló (`caseRevision`). Mientras la
ejecución está abierta, una edición del caso pone `caseUpdated: true` en los casos de la ejecución
que siguen pendientes de ejecutar (`untested`, o `retested` tras un reintento); el caso de la
ejecución conserva el contenido anterior hasta que [apliques la última versión](#aplicar-la-última-versión-del-caso).
Un caso de la ejecución completado y una ejecución cerrada nunca informan una actualización.

### Ejemplo

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/runs/01J...RUN/cases"
```

```json
{
  "cases": [
    {
      "runCaseUlid": "01J...RC",
      "caseUlid": "01J...CASE",
      "caseNumber": 7,
      "titleSnapshot": "Flujo de pago",
      "status": "untested",
      "caseLifecycleStatus": {
        "optionUlid": "01J...OPT",
        "optionName": "Borrador",
        "optionColor": "#F59E0B",
        "optionIcon": "circle-dashed",
        "optionSystemKey": "status_draft"
      },
      "customFieldValues": [
        {
          "fieldUlid": "01J...FLD",
          "fieldName": "Prioridad",
          "optionUlid": "01J...PRI",
          "optionName": "Alta",
          "optionColor": "#EF4444",
          "optionIcon": null,
          "optionSystemKey": null
        }
      ]
    }
  ]
}
```

Cuando el caso fuente no tiene estado de ciclo de vida configurado, o el caso fue eliminado:

```json
{
  "caseLifecycleStatus": null
}
```

Cuando una entrada de `customFieldValues` pertenece a una **opción de sistema** (campo de estado del ciclo de vida):

```json
{
  "optionSystemKey": "status_active"
}
```

Cuando pertenece a una **opción personalizada** (definida por el usuario):

```json
{
  "optionSystemKey": null
}
```

## Listar las claves de casos de una ejecución

```
GET /api/v1/runs/{runUlid}/case-keys
```

Devuelve cada caso que está ahora en la ejecución con solo lo que un reporter de CI necesita para
ejecutar las pruebas vinculadas: su id de visualización y su
[clave de automatización](https://docs.probara.net/es/developers/api-reference/#clave-de-automatización). Un reporter de
framework lo usa para ejecutar solo las pruebas de los casos de una ejecución existente.

| Campo           | Tipo             | Descripción                                                 |
| --------------- | ---------------- | ----------------------------------------------------------- |
| `caseDisplayId` | `string`         | El id de visualización del caso, por ejemplo `ACME-14`      |
| `automationKey` | `string \| null` | La clave de automatización del caso, `null` si no tiene una |

- Un caso quitado de la ejecución no aparece, ni tampoco un caso eliminado del proyecto.
- Los elementos se ordenan por ULID del caso y se paginan por cursor: `limit` (por defecto 50,
  máximo 200; por encima responde `422 validation_failed`) y `cursor`, el `nextCursor` de la página
  anterior. `nextCursor` es `null` en la última página.
- Las ejecuciones cerradas y abortadas se pueden leer.
- Es la única lectura que puede hacer un [token de aplicación](https://docs.probara.net/es/developers/authentication/#llamadas-permitidas).
  Un token de aplicación solo lee ejecuciones de su organización; cualquier otra responde `404 not_found`,
  y en una organización sin plan de pago responde `403 forbidden`, como toda llamada con token de aplicación.
- Una ejecución fuera de la organización de quien llama responde `404 not_found`. Una sesión o un
  token OAuth además necesitan acceso al proyecto de la ejecución y sin él reciben `403 forbidden`;
  un token de API personal o un token de aplicación llegan a todos los proyectos de su organización,
  incluidos los privados.

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_APP_TOKEN" \
  "https://app.probara.net/api/v1/runs/01J...RUN/case-keys?limit=200"
```

```json
{
  "items": [
    { "caseDisplayId": "ACME-14", "automationKey": "cart > adds an item" },
    { "caseDisplayId": "ACME-15", "automationKey": null }
  ],
  "nextCursor": null
}
```

## Obtener un caso de una ejecución

```
GET /api/v1/runs/{runUlid}/cases/{runCaseUlid}
```

Devuelve el caso de ejecución con sus pasos, `linkedDefects` y lo que corresponde a su **intento
actual** (el último resultado del caso en la ejecución): `resultUlid`, `resultAttachments` y, para
un resultado que registró un reporte de CI, lo que reportó la ejecución:

| Campo         | Descripción                                                                                                                                                                                                                                                                                                                               |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `parameters`  | Los parámetros que reportó el intento actual, `[{ name, value }]` en el orden enviado                                                                                                                                                                                                                                                     |
| `links`       | Los enlaces que reportó el intento actual, `[{ url, name }]` en el orden enviado (`name` es `null` si no se envió)                                                                                                                                                                                                                        |
| `resultSteps` | Los pasos que reportó el intento actual, aplanados en profundidad y en preorden: `{ position, parentPosition, action, status, durationMs, error, expected, data }`, donde `position` es el índice del paso desde 0 (el `stepIndex` de un elemento de `resultAttachments`) y `parentPosition` el de su padre (`null` en el nivel superior) |

Los tres son `[]` cuando el intento actual no reportó ninguno — un resultado marcado en la aplicación
web, o un caso que nunca se marcó — y siguen al intento actual: un intento posterior que no reportó
nada los vacía. Son las mismas listas que devuelve la
[instantánea del intento](https://docs.probara.net/es/developers/api/run-result-snapshots/) para `resultUlid`.
`resultSteps` no es `steps`: `steps` son los pasos propios del caso. Cada elemento de `steps` incluye `keyword`, la palabra clave Gherkin de un paso de un caso Gherkin, congelada con el paso; es `null` en un paso clásico.

## Marcar un caso

```
PATCH /api/v1/runs/{runUlid}/cases/{runCaseUlid}
```

Define `assigneeUlid` / `status` / `elapsedMs`. Un cambio de `status` marca todos los pasos y
agrega una fila de resultado; el `resultUlid` de la respuesta lleva el intento agregado (`null`
en un patch que solo cambia el asignado o el tiempo transcurrido, sin cambiar el estado).
Requiere `member`+ en el proyecto de la ejecución; `viewer` recibe `403 forbidden`.

### Comportamiento en una ejecución cerrada

Permitido en una ejecución **abierta** o **cerrada sin abortar** cuando la configuración de
ejecución `allowResultsInClosedRuns` del proyecto (ver
[Configuración de ejecución](https://docs.probara.net/es/developers/api/project-run-settings/)) resuelve a `true` (el
valor por defecto del código). Una organización o proyecto que lo fije en `false` restaura el
comportamiento anterior, más estricto: una ejecución cerrada rechaza el marcado con
`409 conflict`. Una ejecución **abortada** siempre lo rechaza, sin importar el valor de esta
configuración.

### `403 run_case_assignee_locked`

Cuando la configuración `assigneeResultLock` del proyecto está activa y el caso tiene un
asignado, solo ese asignado — o un solicitante exento por `projects.manage` o por ser el
propietario actual del proyecto — puede marcarlo. Cualquier otro solicitante es rechazado,
**incluido un token de API**: no existe una exención para solicitantes automatizados sobre este
bloqueo.

```json
{
  "error": {
    "code": "run_case_assignee_locked",
    "message": "Only the assigned tester may record a result for this case"
  }
}
```

### `422 elapsed_ms_required`

Cuando la configuración `timeTracking` del proyecto es `"required"` y la solicitud define un
`status` terminal sin `elapsedMs`, el marcado se rechaza y no se persiste nada:

```json
{
  "error": {
    "code": "validation_failed",
    "message": "elapsedMs is required",
    "details": { "code": "elapsed_ms_required", "path": ["elapsedMs"] }
  }
}
```

Con `"off"` u `"optional"`, un `elapsedMs` provisto se sigue aceptando y persistiendo — la
configuración solo gobierna si se muestra la interfaz de seguimiento de tiempo del cliente web y
si se rechaza un marcado sin `elapsedMs`, nunca si se descarta un valor provisto.

**El marcado en bloque y el envío de resultados en bloque están exentos de este rechazo** — ni
`POST /runs/{runUlid}/cases/bulk-mark` ni `POST /runs/{runUlid}/cases/bulk-submit-result` llevan
un campo `elapsedMs` por caso que el servidor pudiera exigir, así que `timeTracking: "required"`
nunca bloquea ninguno de los dos endpoints en bloque.

Consulta [Configuración de ejecución](https://docs.probara.net/es/developers/api/project-run-settings/) para el
vocabulario completo de configuraciones y la cadena de resolución.

## Aplicar la última versión del caso

```
POST /api/v1/runs/{runUlid}/cases/{runCaseUlid}/apply-latest
POST /api/v1/runs/{runUlid}/cases/apply-latest
```

Lleva a la última versión de su caso un caso de la ejecución cuyo caso de prueba se editó después
de que la ejecución lo congelara: se reemplazan su título, suite, descripción, precondiciones,
postcondiciones, prioridad y pasos, y `caseRevision` pasa a ser la revisión actual del caso. El
responsable, la posición y el estado no cambian. Envía un cuerpo JSON vacío (`{}`). Requiere
`member`+ en el proyecto de la ejecución, igual que marcar un caso.

Un caso de la ejecución solo puede tomar la última versión mientras está **pendiente**
(`untested`, o `retested` tras un reintento) y su **intento actual no tiene resultados de pasos**:
ningún paso con estado, resultado real o adjunto. Para actualizar un caso completado, reinténtalo
primero (`POST /api/v1/runs/{runUlid}/cases/{runCaseUlid}/retry`). Los intentos anteriores conservan el título, los campos, los
pasos, los resultados de los pasos y los adjuntos con los que se ejecutaron, así que cada resultado
registrado sigue correspondiendo a los pasos que realmente se ejecutaron. `canApplyLatest` lo
indica de antemano.

El endpoint de un caso responde con el caso de la ejecución y sus pasos, como `GET`. Rechaza con
`409 conflict` y no cambia nada en los casos siguientes. Se comprueban en este orden y se informa
el primero que se cumple:

| Situación                                                                                       | `error.message` / `error.details.reason` |
| ----------------------------------------------------------------------------------------------- | ---------------------------------------- |
| La ejecución está cerrada                                                                       | `run is closed`                          |
| El caso fuente fue eliminado                                                                    | `case_deleted`                           |
| El caso fuente no cambió desde que este caso de la ejecución lo congeló, sea cual sea su estado | `case_not_outdated`                      |
| El caso de la ejecución está completado o su intento tiene resultados de pasos                  | `run_case_has_results`                   |

`case_not_outdated` compara `caseRevision` con `caseCurrentRevision`; no depende de `caseUpdated`,
que es `false` para todo caso de la ejecución completado. Un caso de la ejecución completado cuyo
caso fuente cambió responde `run_case_has_results`: reinténtalo primero.

Cuando `assigneeResultLock` está activado en el proyecto, se aplica la misma regla
[`403 run_case_assignee_locked`](#403-run_case_assignee_locked) que al marcar.

El endpoint de toda la ejecución aplica la última versión a cada caso con `caseUpdated: true`
cuyo intento actual no tiene resultados de pasos, en una sola transacción, e informa los casos
pendientes desactualizados que dejó sin cambios (los casos completados nunca son candidatos):

```json
{
  "updated": ["01J...RC1"],
  "skipped": [{ "runCaseUlid": "01J...RC2", "reason": "run_case_has_results" }]
}
```

`reason` es `run_case_has_results`, o `assignee_locked` cuando el bloqueo por responsable impide
al llamante cambiar ese caso. Una ejecución cerrada responde `409 conflict` (`run is closed`).

Cada caso actualizado registra `run.case_version_applied` (`fromRevision`, `toRevision`); la
llamada de toda la ejecución registra un único `run.cases_bulk_version_applied` con los casos
actualizados, o nada si no se actualizó ninguno. Consulta la
[API de eventos de auditoría](https://docs.probara.net/es/developers/api/audit-events/).

## Reconciliar la selección de casos de una ejecución

```
PATCH /api/v1/runs/{runUlid}/cases
```

Reconcilia la selección de casos de una ejecución a la **selección completa
deseada** que se envíe, en una sola transacción atómica. A diferencia de
`POST /runs/{runUlid}/cases` (solo agrega — añade los ULIDs de caso dados y
nunca quita nada), este endpoint trata la ausencia del conjunto deseado como
una **eliminación**. Ambos endpoints siguen disponibles y no cambian entre sí.

Requiere `member`+ (el mismo permiso de ejecución que correr los tests);
`viewer` recibe `403`. Permitido en una ejecución **abierta** o **cerrada sin
abortar** (completada, o cerrada automáticamente al tener resultado todos sus
casos); rechaza solo una ejecución **abortada** con `409`, sin mutar nada.

### Cuerpo de la solicitud

```json
{
  "caseUlids": ["01J...A", "01J...B", "01J...D"],
  "caseAssignees": [{ "caseUlid": "01J...D", "assigneeUlid": "01J...USER" }]
}
```

| Campo           | Tipo               | Descripción                                                                                                                                                                                    |
| --------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `caseUlids`     | `string[]`         | La selección completa deseada (ULIDs de caso fuente). Un **array vacío es válido** — elimina todos los casos de la ejecución.                                                                  |
| `caseAssignees` | `array` (opcional) | Asignado por caso para la selección deseada. Un ULID de caso **ausente** de este array significa que ese caso queda (o pasa a quedar) sin asignar — no existe un valor explícito de "limpiar". |

Dada la selección actual y la solicitud anterior, el servidor calcula la
diferencia y la aplica, de forma atómica:

- **Agrega** cada caso de `caseUlids` que aún no está en la ejecución —
  congelando su título y pasos como un snapshot nuevo, `status: "untested"`,
  con el asignado provisto (si lo hay) aplicado en la misma operación.
- **Elimina** cada caso actual ausente de `caseUlids` — en cascada con sus
  snapshots de pasos y adjuntos de resultado, igual que el endpoint de un solo
  caso `DELETE /runs/{runUlid}/cases/{runCaseUlid}`.
- **Conserva** cada caso presente en ambos, sin cambios (`titleSnapshot`,
  `status`, `assigneeUlid`, `elapsedMs`).
- **Reasigna** cualquier caso conservado cuyo asignado provisto difiera del
  actual.

La respuesta es el resumen reconciliado de la ejecución (`RunResponse`), la
misma forma que devuelve cualquier otra mutación de ejecución — **no**
incluye el array de casos de la ejecución; llama a
`GET /runs/{runUlid}/cases` para obtener el listado actualizado.

### Validación del asignado

Cada `assigneeUlid` provisto debe resolver a un miembro de la organización de
la ejecución. **Un ULID de asignado ajeno a la organización o desconocido
devuelve `404 not_found`** — igual que el comportamiento existente de la
creación (`POST /projects/{projectId}/runs`) y del patch por caso
(`PATCH /runs/{runUlid}/cases/{runCaseUlid}`) para el mismo campo, no un
`422 validation_failed`. En ese caso no se agrega, elimina ni reasigna ningún
caso.

### Eventos de auditoría

Emite exactamente un evento por cada caso que realmente cambió:
`run.case_added` por cada caso agregado, `run.case_removed` por cada caso
eliminado, `run.case_assigned` por cada caso cuyo asignado cambió (incluyendo
una limpieza, `to: null`) — ver [API de eventos de auditoría](https://docs.probara.net/es/developers/api/audit-events/)
para las formas compartidas de metadata. Un caso sin cambios tanto en
membresía como en asignado no emite nada. El asignado incorporado de un caso
recién agregado se registra en `run.case_added` y **no** emite además un
`run.case_assigned`.

### Ejemplo

```bash
curl -sS -X PATCH \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"caseUlids": ["01J...A", "01J...D"]}' \
  "https://app.probara.net/api/v1/runs/01J...RUN/cases"
```

## Solo casos aprobados

Cuando la [configuración de revisión](https://docs.probara.net/es/developers/api/project-review-settings/) del proyecto tiene
`onlyApprovedInRuns` activada (plan Pro o Enterprise, con la revisión `enabled`), una ejecución solo admite
casos de prueba con `reviewStatus` `approved`:

- **Los casos que eliges** — `caseUlids` en `POST /api/v1/projects/{projectId}/runs`, en
  `POST /runs/{runUlid}/cases` y los casos que añade un `PATCH /runs/{runUlid}/cases` — deben estar
  todos aprobados. Si no, se rechaza la solicitud entera y nada cambia:

  ```json
  {
    "error": {
      "code": "validation_failed",
      "message": "only approved test cases can be added to runs in this project",
      "details": {
        "reason": "cases_not_approved",
        "notApprovedCaseUlids": ["01J...B"]
      }
    }
  }
  ```

  Una reconciliación conserva los casos que ya están en la ejecución aunque hayan dejado de estar
  aprobados.

- **Los casos que una ejecución toma por su cuenta** — la selección de un plan de pruebas cuando se
  omite `caseUlids`, y los casos de la ejecución de origen de un clon — se filtran: solo se añaden
  los aprobados, y la respuesta incluye `notApprovedSkippedCount`, cuántos quedaron fuera. Si
  ninguno está aprobado, la solicitud se rechaza con `422 validation_failed` (`details.code`
  `empty_selection`).

Con la opción desactivada, la revisión desactivada o un plan sin revisión de casos, nada cambia y
`notApprovedSkippedCount` no aparece. Los reportes de CI (`POST /api/v1/projects/{projectId}/reports`)
no se ven afectados.

## Límites de solicitud

Cada uno de los siguientes tres endpoints limita el arreglo `caseUlids` en el
cuerpo de la solicitud. Son techos contra abuso, no techos de producto — el
servidor procesa cualquier tamaño aceptado sin fallar por límite de
parámetros. Enviar más del límite responde `422 validation_failed` nombrando
el campo `caseUlids`, y no muta nada.

| Endpoint                                                   | Campo       | Límite |
| ---------------------------------------------------------- | ----------- | ------ |
| `POST /api/v1/runs/{runUlid}/cases` (agregar casos)        | `caseUlids` | 500    |
| `PATCH /api/v1/runs/{runUlid}/cases` (reconciliar)         | `caseUlids` | 5000   |
| `POST /api/v1/projects/{projectId}/runs` (crear ejecución) | `caseUlids` | 5000   |

## Casos de ejecución parametrizados

Un caso parametrizado tiene un caso de ejecución por cada combinación, y cada caso de ejecución lleva su combinación en `parameters`. La lista de casos de la ejecución la devuelve en cada elemento (`[]` para un caso sin parámetros). El detalle del caso de ejecución añade `siblings`, los demás casos de ejecución del mismo caso en la ejecución, con su estado, su responsable y sus parámetros, y `reportedParameters`, los parámetros que reportó el intento actual.

`POST /api/v1/runs/{runUlid}/cases/{runCaseUlid}/apply-latest` se rechaza con `409 conflict` y `details.reason: combination_removed` cuando la última versión del caso ya no produce la combinación del caso de ejecución. La aplicación en lote, `POST /api/v1/runs/{runUlid}/cases/apply-latest`, muestra el mismo motivo en `skipped`.

`GET /api/v1/runs/{runUlid}/case-keys` devuelve una entrada por cada caso de ejecución, con sus `parameters`, para que un reporter distinga los casos de ejecución de un mismo caso.

Para las reglas detrás de estos campos, consulta [Casos de prueba parametrizados](https://docs.probara.net/es/guides/parameterized-test-cases/) y la [referencia de la API](https://docs.probara.net/es/developers/api-reference/#casos-de-prueba-parametrizados).

## Relacionado

- [Referencia API](https://docs.probara.net/es/developers/api-reference/) — paginación, errores, autenticación
- [Referencia interactiva v1](https://docs.probara.net/es/developers/api-reference/v1/) — esquemas completos
- [API de búsqueda de casos](https://docs.probara.net/es/developers/api/test-case-search/) — búsqueda entre suites con `customFieldValues`
