# API de planes de prueba

> Crea, gestiona y genera ejecuciones a partir de planes de prueba por proyecto mediante la API de integración.

Los planes de prueba son colecciones de casos de prueba seleccionados dentro de un proyecto. Cada plan contiene una selección ordenada de casos activos; desde esa selección puedes generar una ejecución de prueba que toma una instantánea de los casos en el momento de la generación. Cada plan tiene un ULID asignado por el servidor, un número de plan asignado de forma monotónica dentro del proyecto y una descripción opcional.

## Crear

`POST /api/v1/projects/{projectId}/plans`

`{projectId}` es el **código** del proyecto (por ejemplo, `ACME`).

### Cuerpo de la solicitud

| Campo         | Requerido | Notas                                                                                        |
| ------------- | --------- | -------------------------------------------------------------------------------------------- |
| `name`        | sí        | Cadena no vacía, máximo 200 caracteres. Debe ser único entre los planes activos del proyecto |
| `description` | no        | Descripción en texto libre, máximo 2000 caracteres                                           |
| `status`      | no        | Estado inicial: `draft`, `active` o `completed`. Por defecto es `draft` si se omite          |

Los campos `ulid` o `planNumber` provistos por el cliente son rechazados. Un nombre duplicado entre planes activos del proyecto devuelve `409 conflict`. Campos faltantes o inválidos devuelven `422 validation_failed`.

### Respuesta

`201` con el objeto del plan creado. Ver [Campos de respuesta](#campos-de-respuesta).

### Ejemplo

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Sprint 24","description":"Lanzamiento de funcionalidad"}' \
  "https://app.probara.net/api/v1/projects/ACME/plans"
```

## Listar

`GET /api/v1/projects/{projectId}/plans`

Devuelve los planes activos (no eliminados) del proyecto, ordenados por número de plan ascendente. Paginación por página.

### Parámetros de consulta

| Parámetro  | Notas                                                    |
| ---------- | -------------------------------------------------------- |
| `page`     | Número de página (base 1, valor predeterminado `1`)      |
| `pageSize` | Elementos por página (predeterminado `20`, máximo `200`) |

### Respuesta

`200` con `{ items, page, pageSize, total }`. Cada elemento sigue la [estructura de respuesta](#campos-de-respuesta).

### Ejemplo

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/ACME/plans?page=1&pageSize=20"
```

## Obtener

`GET /api/v1/plans/{planUlid}`

Devuelve un plan activo perteneciente a la organización activa, incluyendo el conteo actual de casos.

Devuelve `404 not_found` cuando el plan está fuera de la organización activa o ha sido eliminado.

### Ejemplo

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/plans/01HZPLANULID00000000000000"
```

## Actualizar

`PATCH /api/v1/plans/{planUlid}`

Actualiza uno o más campos de un plan activo. Se debe proporcionar al menos un campo. Un nombre duplicado devuelve `409 conflict`.

### Cuerpo de la solicitud

| Campo         | Requerido | Notas                                                                                                                                                                                                                                                                                         |
| ------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`        | no        | Nuevo nombre (máximo 200 caracteres). Debe ser único entre planes activos del proyecto                                                                                                                                                                                                        |
| `description` | no        | Descripción actualizada (máximo 2000 caracteres). Enviar `null` para borrarla                                                                                                                                                                                                                 |
| `status`      | no        | Nuevo estado del ciclo de vida: `draft`, `active` o `completed`. Cualquier transición entre estados distintos es válida. Enviar el estado actual del plan es un no-op (aceptado, sin evento). Un valor de estado inválido devuelve `422 validation_failed`                                    |
| `milestoneId` | no        | ULID de un hito activo del mismo proyecto para vincular, o `null` para borrar el vínculo existente. Un ULID que no corresponde a un hito activo del proyecto del plan devuelve `404 not_found` y deja el plan sin cambios. Un hito de otro proyecto u organización también es `404 not_found` |

Un cuerpo vacío (sin campos) devuelve `422 validation_failed`.

### Respuesta

`200` con el objeto del plan actualizado. Ver [Campos de respuesta](#campos-de-respuesta).

### Reglas de transición de estado

- Todas las transiciones entre `draft`, `active` y `completed` son válidas en cualquier dirección.
- No hay estados terminales: un plan `completed` puede volver a `active` o `draft`.
- Un PATCH con el mismo valor de estado (el campo `status` coincide con el estado actual del plan) se acepta como no-op — no se registra ningún cambio de estado y el plan devuelve `200` sin modificaciones.
- Un valor de estado inválido (que no sea uno de los tres valores válidos) devuelve `422 validation_failed`.
- El estado es estrictamente manual: la API nunca avanza el estado de un plan automáticamente según el estado de sus ejecuciones.

### Ejemplos

```bash
# Cambiar el estado a activo
curl -sS -X PATCH \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"active"}' \
  "https://app.probara.net/api/v1/plans/01HZPLANULID00000000000000"
```

```bash
# Vincular un hito
curl -sS -X PATCH \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"milestoneId":"01HZMILESTONE0000000000000"}' \
  "https://app.probara.net/api/v1/plans/01HZPLANULID00000000000000"
```

```bash
# Borrar el vínculo de hito
curl -sS -X PATCH \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"milestoneId":null}' \
  "https://app.probara.net/api/v1/plans/01HZPLANULID00000000000000"
```

```bash
# Combinado: renombrar y marcar como completado
curl -sS -X PATCH \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Sprint 25 Final","status":"completed"}' \
  "https://app.probara.net/api/v1/plans/01HZPLANULID00000000000000"
```

## Eliminar

`DELETE /api/v1/plans/{planUlid}`

Elimina el plan de forma lógica. El plan deja de aparecer en la lista y su nombre queda disponible para reutilizarse. Las ejecuciones generadas a partir del plan no se ven afectadas.

Devuelve `204` en caso de éxito, `404 not_found` si no se encuentra.

### Ejemplo

```bash
curl -sS -X DELETE \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/plans/01HZPLANULID00000000000000"
```

## Reemplazar la selección de casos

`PUT /api/v1/plans/{planUlid}/cases`

Semántica de reemplazo total: la **selección completa** se reemplaza por la lista ordenada de ULIDs de casos proporcionada. Omitir un caso de la lista lo elimina; enviarlo en una posición diferente lo mueve. Envía un array `caseUlids` vacío para vaciar la selección.

Opcionalmente puedes asignar un miembro del equipo a cada caso en la misma llamada enviando `caseAssignees`. Las entradas cuyo `caseUlid` no aparezca en `caseUlids` se ignoran silenciosamente. La resolución de todos los responsables ocurre **antes** de que se reemplace la selección: un `assigneeUlid` inválido deja la selección actual sin cambios.

### Cuerpo de la solicitud

| Campo           | Requerido | Notas                                                                                                                                                                                                                                                                                                                                                                                        |
| --------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `caseUlids`     | sí        | Array ordenado de ULIDs de casos activos, **como máximo 5000 entradas**. Los ULIDs duplicados son rechazados (`422`). Los casos deben pertenecer al proyecto del plan                                                                                                                                                                                                                        |
| `caseAssignees` | no        | Array de pares `{ caseUlid, assigneeUlid }`. Cada `assigneeUlid` debe ser miembro de la organización activa — un `assigneeUlid` de otra organización devuelve `404 not_found` y deja la selección sin cambios. Las entradas cuyo `caseUlid` no aparezca en `caseUlids` se ignoran. Omitir este campo (o no incluir una entrada para un caso en particular) almacena ese caso sin responsable |

### Límites de solicitud

`caseUlids` tiene un límite de **5000 entradas** — un techo contra abuso, no un techo de producto; el servidor fragmenta sus sentencias con cualquier tamaño aceptado. Enviar más responde `422 validation_failed` nombrando el campo `caseUlids`, y la selección del plan queda sin cambios.

Devuelve `404 not_found` si algún ULID no corresponde a un caso activo del proyecto o si algún `assigneeUlid` no es miembro de la organización. Devuelve `422 validation_failed` por ULIDs duplicados o por un array `caseUlids` con más de 5000 entradas.

### Respuesta

`200` con `{ items }` — la nueva selección en orden de posición. Ver [Campos de elemento de selección](#campos-de-elemento-de-selección).

### Ejemplo — solo selección de casos

```bash
curl -sS -X PUT \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"caseUlids":["01HZCASEA","01HZCASEB","01HZCASEC"]}' \
  "https://app.probara.net/api/v1/plans/01HZPLANULID00000000000000/cases"
```

### Ejemplo — selección con responsables por caso

```bash
curl -sS -X PUT \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "caseUlids": ["01HZCASEA","01HZCASEB","01HZCASEC"],
    "caseAssignees": [
      { "caseUlid": "01HZCASEA", "assigneeUlid": "01HZMEMBER000000000000000001" },
      { "caseUlid": "01HZCASEB", "assigneeUlid": "01HZMEMBER000000000000000002" }
    ]
  }' \
  "https://app.probara.net/api/v1/plans/01HZPLANULID00000000000000/cases"
```

`01HZCASEC` no tiene entrada en `caseAssignees` y se almacena sin responsable.

## Actualizar el responsable de un caso del plan

`PATCH /api/v1/plans/{planUlid}/cases/{planCaseUlid}`

Actualiza el **responsable** de un único caso del plan sin modificar el resto de la selección. Este endpoint es la actualización en línea por caso — no reemplaza la lista de casos del plan.

**Semántica de instantánea**: actualizar el responsable de un caso del plan no se propaga a ninguna ejecución ya generada desde el plan. Los casos de ejecución existentes conservan el responsable que tenían en el momento en que se generó la ejecución.

### Parámetros de ruta

| Parámetro      | Descripción                                                                                                      |
| -------------- | ---------------------------------------------------------------------------------------------------------------- |
| `planUlid`     | ULID del plan                                                                                                    |
| `planCaseUlid` | ULID de la fila del caso en el plan (el campo `ulid` de un elemento de selección, no el ULID del caso de prueba) |

### Cuerpo de la solicitud

| Campo          | Requerido | Notas                                                                             |
| -------------- | --------- | --------------------------------------------------------------------------------- |
| `assigneeUlid` | sí        | ULID de un miembro activo de la organización, o `null` para quitar el responsable |

El cuerpo debe contener exactamente una clave. Las claves desconocidas devuelven `422 validation_failed`.

### Respuesta

`200` con el objeto del caso del plan actualizado (misma estructura que un elemento de selección). Ver [Campos de elemento de selección](#campos-de-elemento-de-selección).

### Códigos de error

| Estado                     | Código              | Cuándo                                                                   |
| -------------------------- | ------------------- | ------------------------------------------------------------------------ |
| `403 Forbidden`            | —                   | El llamante tiene rol `viewer`                                           |
| `404 Not Found`            | `not_found`         | `planUlid` o `planCaseUlid` no existe o pertenece a otra organización    |
| `404 Not Found`            | `not_found`         | `assigneeUlid` no corresponde a un miembro activo de la organización     |
| `422 Unprocessable Entity` | `validation_failed` | Clave `assigneeUlid` faltante, valor que no es ULID, o clave desconocida |

### Ejemplo

```bash
# Asignar un responsable a un caso del plan
curl -sS -X PATCH \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"assigneeUlid":"01HZMEMBER000000000000000001"}' \
  "https://app.probara.net/api/v1/plans/01HZPLANULID00000000000000/cases/01HZPLANCASE000000000000000"

# Quitar el responsable (enviar null)
curl -sS -X PATCH \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"assigneeUlid":null}' \
  "https://app.probara.net/api/v1/plans/01HZPLANULID00000000000000/cases/01HZPLANCASE000000000000000"
```

## Listar la selección de casos

`GET /api/v1/plans/{planUlid}/cases`

Devuelve la selección actual de casos del plan, ordenada por posición ascendente.

### Respuesta

`200` con `{ items }`. Ver [Campos de elemento de selección](#campos-de-elemento-de-selección).

### Ejemplo

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

## Reemplazar la selección de configuraciones

`PUT /api/v1/plans/{planUlid}/configurations`

Semántica de reemplazo total: la **selección completa** de configuraciones del plan se reemplaza por la lista de ULIDs de valores de configuración proporcionada. Envía un array `configurationUlids` vacío para vaciar la selección.

Cada ULID proporcionado debe corresponder a un valor de configuración activo (no eliminado) en el proyecto del plan. Un ULID no resuelto, eliminado de forma lógica o de otro proyecto devuelve `404 not_found` y deja la selección sin cambios. Los ULIDs duplicados en la solicitud devuelven `422 validation_failed`.

### Cuerpo de la solicitud

| Campo                | Requerido | Notas                                                                                                                                        |
| -------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `configurationUlids` | sí        | Array de ULIDs de valores de configuración activos. Los duplicados son rechazados (`422`). Los valores deben pertenecer al proyecto del plan |

### Respuesta

`200` con `{ items }` — la nueva selección de configuraciones. Ver [Campos de elemento de configuración](#campos-de-elemento-de-configuración).

### Ejemplo

```bash
curl -sS -X PUT \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"configurationUlids":["01HZCHROME000000000000000000","01HZWINDOWS00000000000000000"]}' \
  "https://app.probara.net/api/v1/plans/01HZPLANULID00000000000000/configurations"
```

## Listar la selección de configuraciones

`GET /api/v1/plans/{planUlid}/configurations`

Devuelve la selección de configuraciones actual del plan — el conjunto de ULIDs de valores de configuración que el plan usa para calcular su matriz de combinaciones.

### Respuesta

`200` con `{ items }`. Ver [Campos de elemento de configuración](#campos-de-elemento-de-configuración).

### Ejemplo

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/plans/01HZPLANULID00000000000000/configurations"
```

## Establecer combinaciones excluidas

`PUT /api/v1/plans/{planUlid}/excluded-combinations`

Semántica de reemplazo total: las claves de combinación excluidas del plan se reemplazan exactamente por el conjunto proporcionado. Una `comboKey` que no coincide con ninguna combinación candidata actual es aceptada y almacenada como exclusión inerte — no genera un error.

### Cuerpo de la solicitud

| Campo       | Requerido | Notas                                                                                                                                                                                                                                                                                                                        |
| ----------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `comboKeys` | sí        | Array de cadenas de clave de combinación. Una clave de combinación es la lista de ULIDs de valores de configuración de la combinación, ordenados de forma ascendente y unidos con `:` (por ejemplo, `01HZCHROME:01HZWINDOWS`). Los duplicados son rechazados (`422`). Envía un array vacío para borrar todas las exclusiones |

### Respuesta

`200` con `{ comboKeys }` — el nuevo conjunto de claves de combinación excluidas del plan.

### Ejemplo

```bash
curl -sS -X PUT \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"comboKeys":["01HZFIREFOX0000000000000000:01HZWINDOWS00000000000000000"]}' \
  "https://app.probara.net/api/v1/plans/01HZPLANULID00000000000000/excluded-combinations"
```

## Vista previa de combinaciones

`GET /api/v1/plans/{planUlid}/combinations`

Devuelve las combinaciones candidatas calculadas como el producto cartesiano de la selección de configuraciones actual del plan (un valor por grupo participante). Cada combinación incluye su `comboKey`, los valores por grupo ordenados por posición de grupo, y un indicador `included` (`false` cuando la `comboKey` está en el conjunto de excluidas del plan). La respuesta también devuelve el total de combinaciones candidatas y el conteo de combinaciones incluidas.

Un plan sin selección de configuraciones devuelve exactamente una combinación candidata (la combinación vacía, `totalCount = 1`, `includedCount = 1`).

### Respuesta

`200` con:

| Campo           | Tipo   | Notas                                                                                                         |
| --------------- | ------ | ------------------------------------------------------------------------------------------------------------- |
| `items`         | array  | Objetos de combinación candidata. Ver [Campos de elemento de combinación](#campos-de-elemento-de-combinación) |
| `totalCount`    | number | Número total de combinaciones candidatas (incluyendo las excluidas)                                           |
| `includedCount` | number | Número de combinaciones con `included = true`                                                                 |

### Ejemplo

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/plans/01HZPLANULID00000000000000/combinations"
```

## Crear una ejecución para un plan

No existe `POST /api/v1/plans/{planUlid}/runs`. Para crear una ejecución para un
plan, usa `POST /api/v1/projects/{projectId}/runs`, pasando `planUlid` y, si el
plan tiene configuraciones, los `configurationUlids` de la combinación. Esta
llamada siempre crea **exactamente una** ejecución — vincula un plan, siembra sus
casos y etiqueta una combinación, todo en una sola solicitud. Para cubrir varias
combinaciones, llama una vez por combinación.

Cuando se envía `planUlid` y se omite `caseUlids`, la selección de casos de la
ejecución y sus responsables por caso se siembran desde la selección actual del
plan, siguiendo esta precedencia (gana la primera coincidencia):

1. Una entrada explícita por caso en `caseAssignees` (del cuerpo de la solicitud)
2. El responsable almacenado por caso en el plan para ese caso
3. `defaultAssigneeUlid` del cuerpo de la solicitud
4. Sin responsable (`null`)

Un responsable del plan que ya no es miembro de la organización en el momento de
la solicitud resulta en `null` (sin responsable) sin que la solicitud falle.

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Smoke — Chrome","planUlid":"01HZPLANULID00000000000000","configurationUlids":["01HZCFGVALCHROME0000000000"]}' \
  "https://app.probara.net/api/v1/projects/PROJ/runs"
```

Las ejecuciones del plan se listan de la más reciente a la más antigua mediante
`GET /api/v1/plans/{planUlid}/runs`.

## Campos de respuesta

El detalle del plan (`GET /api/v1/plans/{planUlid}`) y la respuesta del `PATCH` devuelven el objeto completo del plan. La lista de planes (`GET /api/v1/projects/{projectId}/plans`) devuelve la misma estructura por elemento pero omite `progress`.

| Campo           | Tipo           | Notas                                                                                          |
| --------------- | -------------- | ---------------------------------------------------------------------------------------------- |
| `ulid`          | string         | ULID asignado por el servidor para el plan                                                     |
| `projectUlid`   | string         | ULID del proyecto al que pertenece el plan                                                     |
| `planNumber`    | number         | Número asignado de forma monotónica dentro del proyecto                                        |
| `name`          | string         | Nombre del plan                                                                                |
| `description`   | string \| null | Descripción opcional                                                                           |
| `author`        | object \| null | Instantánea de autoría (ver más abajo). `null` para registros heredados                        |
| `caseCount`     | number         | Cantidad actual de casos activos en la selección                                               |
| `status`        | string         | Estado actual del ciclo de vida: `draft`, `active` o `completed`                               |
| `milestoneUlid` | string \| null | ULID del hito vinculado, o `null` cuando no hay hito vinculado                                 |
| `milestone`     | object \| null | Referencia al hito vinculado (`{ ulid, name }`), o `null` cuando no hay hito vinculado         |
| `progress`      | object         | Resumen de progreso calculado (solo en el detalle y en la respuesta del PATCH — ver más abajo) |
| `createdAt`     | number         | Marca de tiempo de creación (milisegundos epoch Unix)                                          |
| `updatedAt`     | number         | Marca de tiempo de última actualización (milisegundos epoch Unix)                              |

### Campos de autor

| Campo         | Tipo                                                                                         | Notas                                                                                                                                                              |
| ------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `kind`        | `"user"` \| `"api_token"`                                                                    | Cómo fue creado el plan                                                                                                                                            |
| `ulid`        | string                                                                                       | ULID del usuario o token de API                                                                                                                                    |
| `displayName` | string                                                                                       | Nombre para mostrar (solo tipo usuario)                                                                                                                            |
| `avatarKey`   | string \| null                                                                               | Clave del objeto de avatar (solo tipo usuario)                                                                                                                     |
| `name`        | string                                                                                       | Nombre del token (solo tipo api_token)                                                                                                                             |
| `app`         | `"junit"` \| `"playwright"` \| `"jest"` \| `"cypress"` \| `"vitest"` \| `"cucumber"` \| null | La aplicación de Integraciones de un [token de aplicación](https://docs.probara.net/es/developers/authentication/#tokens-de-aplicación); `null` para un token de API (solo tipo api_token) |

### Campos de progreso

`progress` se calcula en el momento de la lectura a partir de las ejecuciones del plan. Está presente en el detalle del plan (`GET /api/v1/plans/{planUlid}`) y en la respuesta del `PATCH`. La **lista** de planes no incluye `progress`.

| Campo             | Tipo   | Notas                                                                                           |
| ----------------- | ------ | ----------------------------------------------------------------------------------------------- |
| `totalRuns`       | number | Número total de ejecuciones generadas desde este plan                                           |
| `closedRuns`      | number | Cantidad de esas ejecuciones que están en estado `closed`                                       |
| `counts`          | object | Conteos de resultados generales sumados de todas las ejecuciones del plan (ver más abajo)       |
| `byConfiguration` | array  | Filas del desglose por configuración (ver más abajo). Vacío cuando el plan no tiene ejecuciones |

#### Conteos de resultados (`counts` y `counts` por fila de desglose)

| Campo      | Tipo   | Notas                                      |
| ---------- | ------ | ------------------------------------------ |
| `passed`   | number | Suma de resultados pasados                 |
| `failed`   | number | Suma de resultados fallidos                |
| `blocked`  | number | Suma de resultados bloqueados              |
| `skipped`  | number | Suma de resultados omitidos                |
| `untested` | number | Suma de resultados sin probar (pendientes) |

Estos valores se suman a partir de los contadores por estado que se mantienen en cada fila de `test_run`. No se hace un re-análisis de los resultados individuales en el momento de la lectura.

#### Desglose por configuración (elementos de `byConfiguration`)

Cada elemento de `byConfiguration` corresponde a una combinación de configuración (o al grupo sin configuración):

| Campo        | Tipo   | Notas                                                                                                                                                                                                                        |
| ------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `comboKey`   | string | Clave de combinación: los ULIDs de valores de configuración de la ejecución ordenados de forma ascendente y unidos con `:`. Cadena vacía (`""`) para el grupo sin configuración (ejecuciones sin etiquetas de configuración) |
| `values`     | array  | Entradas de valor por grupo ordenadas (`{ groupName, valueName }`). Array vacío para el grupo sin configuración                                                                                                              |
| `totalRuns`  | number | Número de ejecuciones en esta combinación                                                                                                                                                                                    |
| `closedRuns` | number | Número de esas ejecuciones que están cerradas                                                                                                                                                                                |
| `counts`     | object | Conteos de resultados para esta combinación (misma estructura que `counts` a nivel general)                                                                                                                                  |

La suma de todos los `byConfiguration[*].counts` es igual al `counts` de nivel general. Las ejecuciones sin etiquetas de configuración aparecen en un único grupo sin configuración (con `comboKey` vacío y `values` vacío).

## Campos de elemento de selección

Devueltos por `GET /plans/{planUlid}/cases` y `PUT /plans/{planUlid}/cases`.

| Campo          | Tipo           | Notas                                                                                          |
| -------------- | -------------- | ---------------------------------------------------------------------------------------------- |
| `ulid`         | string         | ULID asignado por el servidor para la fila de selección                                        |
| `caseUlid`     | string         | ULID del caso de prueba referenciado                                                           |
| `displayId`    | string         | ID de visualización del caso (por ejemplo, `TC-42`)                                            |
| `title`        | string         | Título actual del caso de prueba                                                               |
| `suiteName`    | string \| null | Nombre del conjunto para agrupar, o `null`                                                     |
| `position`     | number         | Posición dentro de la selección del plan (base 0)                                              |
| `assigneeUlid` | string \| null | ULID del miembro asignado a este caso en el plan, o `null` cuando el caso no tiene responsable |

## Campos de elemento de configuración

Cada elemento en las respuestas de `GET /plans/{planUlid}/configurations` y `PUT /plans/{planUlid}/configurations`:

| Campo               | Tipo   | Notas                                                       |
| ------------------- | ------ | ----------------------------------------------------------- |
| `configurationUlid` | string | ULID del valor de configuración                             |
| `groupUlid`         | string | ULID del grupo de configuración al que pertenece este valor |
| `groupName`         | string | Nombre del grupo (por ejemplo, `Navegador`)                 |
| `valueName`         | string | Nombre del valor (por ejemplo, `Chrome`)                    |

## Campos de elemento de combinación

Cada elemento en la respuesta de `GET /plans/{planUlid}/combinations`:

| Campo      | Tipo    | Notas                                                                                                                                                                                                                 |
| ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `comboKey` | string  | Clave de combinación determinista: los ULIDs de valores de configuración de la combinación ordenados de forma ascendente y unidos con `:`. Usa esta clave en `PUT /excluded-combinations` para excluir la combinación |
| `values`   | array   | Entradas de valor por grupo, ordenadas por posición de grupo. Cada entrada contiene `groupUlid`, `groupName`, `configurationUlid` y `valueName`                                                                       |
| `included` | boolean | `true` cuando esta combinación no está en el conjunto de excluidas del plan; `false` cuando está excluida                                                                                                             |
