# API de solicitudes de revisión de casos

> Envía a revisión un caso de prueba nuevo o un cambio en uno existente en lugar de guardarlo, lista y lee solicitudes de revisión, y apruébalas, edítalas, recházalas o fusiónalas.

Una **solicitud de revisión** propone un cambio en el repositorio de un proyecto: un caso de prueba
nuevo (`create`), un cambio en uno existente (`update`), o archivar (`archive`) o eliminar (`delete`)
uno existente. Enviar la solicitud no cambia nada en el repositorio; la propuesta se aplica cuando se
fusiona la solicitud.

La revisión de casos de prueba se incluye en los planes **Pro** y **Enterprise** (y está disponible durante la
prueba gratuita). En los demás planes estos endpoints, y sus herramientas MCP, responden
`403 forbidden`; `GET /api/v1/orgs/current` incluye `caseReviewEnabled` para comprobarlo antes. El
proyecto debe tener la revisión activada
([Configuración de revisión de casos](https://docs.probara.net/es/developers/api/project-review-settings/)).

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

## Enviar un cambio a revisión

```http
POST /api/v1/projects/{projectId}/test-case-reviews
```

Requiere el permiso `test-cases.write`, el mismo que un guardado directo (hoy lo tienen owner, admin
y member).

Un caso nuevo: `case` es el cuerpo que acepta `POST /api/v1/projects/{projectId}/test-cases`.

```json
{
  "kind": "create",
  "case": {
    "title": "El inicio de sesión funciona con SSO",
    "steps": [{ "position": 1, "action": "Abrir la página de inicio de sesión" }]
  }
}
```

Un cambio en un caso existente: `changes` es el cuerpo que acepta `PATCH /api/v1/test-cases/{caseUlid}`.
Su `expectedRevision` es la revisión del caso sobre la que hiciste el cambio; si se omite, se usa la
revisión actual del caso. En ambos casos queda registrada como `baseRevision` de la solicitud.

```json
{
  "kind": "update",
  "caseUlid": "01J8Z3W4X5Y6Z7A8B9C0D1E2F3",
  "changes": {
    "patch": { "title": "El inicio de sesión funciona con SSO" },
    "expectedRevision": 4
  },
  "reviewerUserIds": ["01J8Z3W4X5Y6Z7A8B9C0D1E2F4"]
}
```

Archivar o eliminar un caso existente: `kind` `archive` o `delete` y el `caseUlid`. La revisión
actual del caso se registra como `baseRevision`; si el caso se guarda (o, en `archive`, se archiva)
antes de fusionar la solicitud, la fusión responde `409` con `details.reason` `review_conflict`.
Estas solicitudes no se pueden editar. Proponer archivar un caso que ya está archivado responde
`409`.

```json
{ "kind": "archive", "caseUlid": "01J8Z3W4X5Y6Z7A8B9C0D1E2F3" }
```

- `reviewerUserIds` (opcional, como máximo 50) indica los revisores. Si se omite, se asignan los
  revisores predeterminados del proyecto. El autor nunca es revisor de su propia solicitud. Cada
  revisor debe ser un miembro actual con acceso al proyecto cuyo rol tenga el permiso
  `test-cases.review`; si no, la solicitud responde `422 validation_failed` con
  `details.reviewerUserIds` enumerando los ULID rechazados, y no se guarda nada.
- Las imágenes preparadas con `POST /api/v1/images:stage` se
  pueden referenciar en los campos de texto enriquecido de la propuesta igual que en un guardado. Se conservan con la solicitud
  mientras siga abierta (la subida preparada caduca a los dos días aproximadamente; la copia de la
  solicitud no).
- La `suiteUlid` y la `milestoneUlid` de un caso nuevo deben pertenecer al proyecto, igual que en una
  creación directa; si no, la solicitud responde `422 validation_failed` y no se guarda nada.
- Cada envío registra una entrada de actividad `test_case_review.created` y avisa a los revisores
  asignados (consulta [Notificaciones](#notificaciones)).

La respuesta es `201` con la solicitud, tal como la devuelve `GET`.

### Rechazos

| Estado | `details.reason`      | Cuándo                                                                                                                                                                                                                                                                 |
| ------ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `403`  | —                     | El plan no incluye la revisión de casos, o el rol no puede escribir casos de prueba.                                                                                                                                                                                   |
| `404`  | —                     | El proyecto, o el caso de un `update`, no existe.                                                                                                                                                                                                                      |
| `409`  | `review_not_enabled`  | El proyecto no tiene la revisión activada.                                                                                                                                                                                                                             |
| `409`  | `review_already_open` | El caso ya tiene una solicitud abierta. `details.reviewUlid` y `details.displayId` la identifican.                                                                                                                                                                     |
| `409`  | `stale_revision`      | `changes.expectedRevision` no es la revisión actual del caso (`details.currentRevision` sí lo es). Vuelve a leer el caso.                                                                                                                                              |
| `409`  | —                     | El caso está sincronizado desde Git (`test_case_synced_read_only`): su escenario cambia en su archivo `.feature`, no mediante revisión. Una solicitud masiva que nombre un caso sincronizado se rechaza completa, y `details.caseUlids` lista los casos sincronizados. |
| `422`  | —                     | Una propuesta no válida, una suite o un hito de otro proyecto, o un revisor no apto.                                                                                                                                                                                   |

## Leer una solicitud de revisión

```http
GET /api/v1/projects/{projectId}/test-case-reviews/{reviewUlid}
```

Disponible para cualquiera que pueda leer el proyecto.

Para referirte a una solicitud por el id `RV-{n}` que muestra la aplicación (y su URL, como `/projects/ACME/review/RV-3`), usa:

```http
GET /api/v1/projects/{projectId}/test-case-reviews/by-display-id/{displayId}
```

Responde con el mismo cuerpo, `ulid` incluido. Un `displayId` que no sea `RV-{n}` responde `422`; uno inexistente, `404`.

```json
{
  "ulid": "01J8Z3W4X5Y6Z7A8B9C0D1E2F5",
  "displayId": "RV-3",
  "kind": "update",
  "status": "open",
  "title": "El inicio de sesión funciona con SSO",
  "caseUlid": "01J8Z3W4X5Y6Z7A8B9C0D1E2F3",
  "case": {
    "ulid": "01J8Z3W4X5Y6Z7A8B9C0D1E2F3",
    "displayId": "ACME-12",
    "title": "El inicio de sesión funciona"
  },
  "baseRevision": 4,
  "proposalVersion": 1,
  "proposal": {
    "kind": "update",
    "changes": {
      "patch": { "title": "El inicio de sesión funciona con SSO" },
      "expectedRevision": 4
    }
  },
  "author": {
    "kind": "user",
    "ulid": "01J8Z3W4X5Y6Z7A8B9C0D1E2F6",
    "displayName": "Ada Lovelace",
    "avatarKey": null
  },
  "reviewers": [
    {
      "userUlid": "01J8Z3W4X5Y6Z7A8B9C0D1E2F4",
      "displayName": "Grace Hopper",
      "avatarKey": null,
      "decision": "pending",
      "proposalVersion": null,
      "current": false,
      "note": null,
      "decidedAt": null
    }
  ],
  "approvalCount": 0,
  "approvalsRequired": 1,
  "allowSelfMerge": false,
  "conflict": false,
  "currentVersionOnly": false,
  "declineNote": null,
  "declineReason": null,
  "createdAt": 1760400000000,
  "updatedAt": 1760400000000,
  "closedAt": null
}
```

| Campo                | Significado                                                                                                                                                                         |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `displayId`          | `RV-{n}`, numerado por proyecto.                                                                                                                                                    |
| `kind`               | `create` (un caso nuevo), `update` (un cambio en `caseUlid`), `archive` o `delete` (archivar o eliminar `caseUlid`).                                                                |
| `status`             | `open`, `merged` o `declined`.                                                                                                                                                      |
| `title`              | El título del caso tal como lo deja la propuesta.                                                                                                                                   |
| `baseRevision`       | La revisión del caso sobre la que se propuso un `update`, `archive` o `delete`; `null` en `create`.                                                                                 |
| `proposalVersion`    | Empieza en `1` y sube cada vez que se edita la propuesta.                                                                                                                           |
| `proposal`           | El cuerpo que se aplica al fusionar la solicitud. Las imágenes preparadas apuntan a las copias que conserva.                                                                        |
| `reviewers`          | La `decision` de cada revisor (`pending`, `approved`, `changes_requested`) y la versión sobre la que la tomó.                                                                       |
| `case`               | El caso que nombra `caseUlid` (su `displayId` y su título actual); `null` en un `create` sin fusionar y cuando el caso se eliminó.                                                  |
| `deletedCase`        | El `displayId` y el `title` del caso cuando se eliminó (al fusionar un `delete` o directamente); `null` mientras existe. `caseUlid` es entonces `null`.                             |
| `approvalCount`      | Las aprobaciones sobre la `proposalVersion` actual. La aprobación del propio autor nunca cuenta.                                                                                    |
| `approvalsRequired`  | Las aprobaciones que el proyecto exige antes de poder fusionar la solicitud (`0`: ninguna).                                                                                         |
| `allowSelfMerge`     | Si el proyecto permite que el autor fusione su propia solicitud.                                                                                                                    |
| `conflict`           | `true` mientras un `update`, `archive` o `delete` abierto no se puede fusionar porque el caso cambió después de proponerlo (o el caso de un `archive` ya está archivado).           |
| `currentVersionOnly` | `true` en un `update` que no propone ningún cambio: pide aprobar el caso tal como está. Consulta [Enviar casos a revisión tal como están](#enviar-casos-a-revisión-tal-como-están). |
| `declineNote`        | La nota que se dejó al rechazar la solicitud; `null` en otro caso.                                                                                                                  |
| `declineReason`      | Por qué el sistema rechazó la solicitud por su cuenta: `case_deleted` cuando se eliminó su caso de prueba. `null` cuando la rechazó una persona, o no está rechazada.               |

El `current` de un revisor es `false` cuando tomó su decisión sobre una versión anterior de la
propuesta: sigue visible pero ya no cuenta. `note` es la nota que dejó con ella. Un `create` fusionado
nombra en `caseUlid` y `case` el caso que creó.

## Listar solicitudes de revisión

```http
GET /api/v1/projects/{projectId}/test-case-reviews?status=open&page=1&pageSize=50
```

Abierto a cualquiera que pueda leer el proyecto. Las más recientes primero, cada una con la forma de
arriba sin `proposal` ni `allowSelfMerge`, en una página (`items`, `page`, `pageSize`, `total`).

| Parámetro       | Significado                                                            |
| --------------- | ---------------------------------------------------------------------- |
| `status`        | `open` (por defecto), `merged`, `declined` o `all`.                    |
| `kind`          | Solo solicitudes de un tipo (`create`, `update`, `archive`, `delete`). |
| `author`        | Solo las solicitudes que envió este miembro (ULID de usuario).         |
| `reviewer`      | Solo las solicitudes que revisa este miembro (ULID de usuario).        |
| `assignedToMe`  | `true`: solo las solicitudes que revisas tú.                           |
| `needsMyReview` | `true`: solo las solicitudes abiertas que esperan tu decisión.         |
| `page`          | Página, desde 1; `pageSize` hasta 200, 50 por defecto.                 |

Una solicitud espera tu decisión mientras eres uno de sus revisores y no aprobaste ni pediste cambios
sobre su `proposalVersion` actual: una decisión que tomaste antes de que se editara la propuesta está
desactualizada y cuenta como no tomada.

`GET /api/v1/projects/{projectId}/test-case-reviews/summary` responde
`{ "enabled": true, "openCount": 3, "needsMyReviewCount": 1 }`: si el proyecto tiene la revisión
activada, cuántas solicitudes están abiertas y cuántas de ellas esperan tu decisión.

## Enviar casos a revisión tal como están

```http
POST /api/v1/projects/{projectId}/test-case-reviews/bulk
```

```json
{
  "caseUlids": ["01J8Z3W4X5Y6Z7A8B9C0D1E2F3", "01J8Z3W4X5Y6Z7A8B9C0D1E2F7"],
  "reviewerUserIds": ["01J8Z3W4X5Y6Z7A8B9C0D1E2F4"]
}
```

Abre una solicitud `update` por caso (de 1 a 200 casos) que no propone ningún cambio, para que los
revisores aprueben el caso tal como está: `currentVersionOnly` es `true`, `proposal.changes` solo
lleva `expectedRevision` y `/versions` responde el mismo caso como `live` y `proposed`. Fusionar la
solicitud marca el caso como `approved` sin escribir una revisión; si el caso cambió mientras tanto,
la fusión responde `409` `review_conflict`. Cada solicitud recibe `reviewerUserIds`, o los revisores
por defecto del proyecto si se omite. Tiene las mismas condiciones que un envío (`test-cases.write`,
revisión activada) y registra un evento de auditoría `test_case_review.created` por solicitud.

```json
{
  "created": [
    { "ulid": "01J8Z3W4X5Y6Z7A8B9C0D1E2F8", "displayId": "RV-7", "currentVersionOnly": true }
  ],
  "skipped": [
    {
      "caseUlid": "01J8Z3W4X5Y6Z7A8B9C0D1E2F7",
      "reason": "review_already_open",
      "reviewUlid": "01J8Z3W4X5Y6Z7A8B9C0D1E2F5",
      "displayId": "RV-3"
    }
  ]
}
```

Responde `201` con `created` (las solicitudes, con la forma de los elementos de la lista) y `skipped`,
cada uno con un `reason`: `review_already_open`, `archived` o `not_found`.

## Leer la versión propuesta

```http
GET /api/v1/projects/{projectId}/test-case-reviews/{reviewUlid}/versions
```

Responde el caso tal como lo deja la propuesta (`proposed`) y, en un `update`, el caso tal como está
ahora (`live`, en `liveRevision`), los dos con la forma de una revisión guardada del caso (el
`snapshot` de `GET /api/v1/test-cases/{caseUlid}/revisions/{revision}`), para que puedas compararlos.
No se escribe nada. Una propuesta que ya no se puede aplicar, por ejemplo porque nombra una suite, un
hito, un campo personalizado o un paso compartido eliminados desde entonces, responde
`proposed: null` y `notApplicable.message` con el motivo; edita la propuesta para corregirla.

## Actuar sobre una solicitud

Todas las acciones de abajo funcionan solo sobre una solicitud **abierta**; sobre una fusionada o
rechazada responden `409` con `details.reason` `review_closed`. Desactivar la revisión en el proyecto
no deja bloqueadas las solicitudes abiertas: se pueden seguir aprobando, editando, rechazando y
fusionando.

| Acción                                  | Quién                                                                                                                                |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `POST .../{reviewUlid}/approve`         | Cualquier miembro cuyo rol tenga `test-cases.review`, salvo el autor.                                                                |
| `POST .../{reviewUlid}/unapprove`       | Cualquier miembro cuyo rol tenga `test-cases.review`, sobre su propia aprobación de la propuesta actual (si no, 409 `not_approved`). |
| `POST .../{reviewUlid}/request-changes` | Cualquier miembro cuyo rol tenga `test-cases.review`, salvo el autor.                                                                |
| `PATCH .../{reviewUlid}` (editar)       | El autor, o cualquier miembro cuyo rol tenga `test-cases.review`.                                                                    |
| `PUT .../{reviewUlid}/reviewers`        | El autor, o cualquier miembro cuyo rol tenga `test-cases.review`.                                                                    |
| `POST .../{reviewUlid}/decline`         | El autor, o cualquier miembro cuyo rol tenga `test-cases.review`.                                                                    |
| `POST .../{reviewUlid}/merge`           | Cualquier miembro cuyo rol tenga `test-cases.review` que no sea el autor; el autor solo si el proyecto le permite fusionar.          |

Cada una responde la solicitud tal como queda después de la acción y registra un evento de auditoría:
`test_case_review.approved`, `.unapproved`, `.changes_requested`, `.edited`, `.reviewers_updated`, `.declined` o
`.merged`.

### Aprobar o pedir cambios

Envía `{}` o `{ "note": "Añade el paso de cierre de sesión" }` (hasta 2000 caracteres). La decisión
se toma sobre la `proposalVersion` actual. Hacerlo te convierte en revisor si no lo eras.

Cuando el proyecto tiene la **fusión automática** activada, una aprobación que lleva la propuesta
actual a las aprobaciones necesarias, sin que ningún revisor pida cambios sobre ella, fusiona la
solicitud en el momento, contigo como quien la fusionó (el evento de fusión lleva `"auto": true`). Si
esa fusión no puede hacerse (porque el caso cambió, por ejemplo), la solicitud sigue abierta con tu
aprobación. La respuesta incluye `autoMergeFailure`: `null`, o `{ "message": "…" }` cuando la fusión
automática falló por un motivo inesperado (la aprobación queda registrada y la solicitud sigue
abierta; vuelve a fusionarla).

### Editar la propuesta

```json
{
  "kind": "update",
  "changes": { "patch": { "title": "El inicio de sesión funciona con SSO y MFA" } }
}
```

El cuerpo lleva el `kind` de la solicitud y su `case` (en `create`) o sus `changes` (en `update`), tal
como se envió. Pasa a ser la siguiente `proposalVersion`, hecha sobre el caso tal como está ahora:
`baseRevision` pasa a la revisión actual del caso, lo que también resuelve un conflicto. Las
decisiones tomadas sobre versiones anteriores dejan de contar. Las nuevas imágenes preparadas se
conservan con la solicitud; las que la edición quita se liberan. Una solicitud `archive` o
`delete` no tiene nada que editar: editarla responde `422`.

### Cambiar los revisores

```json
{ "reviewerUserIds": ["01J8Z3W4X5Y6Z7A8B9C0D1E2F4"] }
```

La lista completa. Un revisor que conservas mantiene su decisión; uno que dejas fuera se quita junto
con ella. Un revisor nuevo debe cumplir las mismas condiciones que al enviar, o la llamada responde
`422` con `details.reviewerUserIds`.

### Rechazar

Cierra la solicitud como `declined`. El caso queda como está, y las imágenes que añadió la
propuesta se liberan. El cuerpo se puede omitir, o llevar `{ "note": "Duplica ACME-4" }` (hasta 2000
caracteres), que se muestra en la solicitud como `declineNote`.

### Fusionar

Aplica la propuesta como un guardado directo: un `create` crea el caso y un `update` guarda el caso
como una nueva revisión cuyo `source` es `review`. También registra el evento de auditoría
`test_case.created` o `test_case.updated` del propio caso. Las imágenes cuentan en el
almacenamiento de la organización como en un guardado, así que una fusión puede responder `413` cuando
el almacenamiento está lleno.

Fusionar exige las aprobaciones necesarias sobre la propuesta actual (en cualquier momento si el
proyecto no exige ninguna). El autor solo puede fusionar si el proyecto permite que el autor fusione, con la
misma regla.

Una solicitud con `currentVersionOnly` no escribe nada en el caso: el caso pasa a `approved` y
conserva su revisión. Una solicitud `archive` archiva el caso (`test_case.archived`; el caso conserva
el estado de revisión que tenía antes de la solicitud) y una `delete` lo elimina
(`test_case.deleted`; responde `409` cuando el caso tiene resultados, como una eliminación directa).

La escritura del caso y el cierre de la solicitud ocurren juntos: una fusión que falla deja el caso y
la solicitud como estaban.

### Rechazos

| Estado | `details.reason`     | Cuándo                                                                                                                                 |
| ------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `403`  | —                    | El plan no incluye la revisión de casos, o no puedes hacer esta acción (ver la tabla de arriba).                                       |
| `404`  | —                    | La solicitud no está en el proyecto.                                                                                                   |
| `409`  | `review_closed`      | La solicitud ya está fusionada o rechazada (`details.status`).                                                                         |
| `409`  | `review_conflict`    | Solo al fusionar: el caso cambió después de hacer la propuesta. Edita la propuesta y luego fusiónala.                                  |
| `409`  | `approvals_required` | Solo al fusionar: la propuesta actual tiene `details.approvalCount` de `details.approvalsRequired` aprobaciones.                       |
| `409`  | —                    | La solicitud cambió mientras se hacía la acción (otra edición o acción llegó antes): léela y vuelve a intentarlo.                      |
| `413`  | —                    | Solo al fusionar: las imágenes superarían el almacenamiento de la organización.                                                        |
| `422`  | —                    | Un cuerpo no válido, una edición de otro `kind`, un revisor que no cumple las condiciones, o una propuesta que ya no se puede aplicar. |

### Decidir sobre varias solicitudes

```http
POST /api/v1/projects/{projectId}/test-case-reviews/bulk-decision
```

```json
{
  "reviewUlids": ["01J8Z3W4X5Y6Z7A8B9C0D1E2F5", "01J8Z3W4X5Y6Z7A8B9C0D1E2F8"],
  "decision": "request_changes",
  "note": "Añade el resultado esperado a cada paso"
}
```

`decision` es `approve`, `request_changes` o `decline`, y se toma sobre cada solicitud (de 1 a 50) en
orden mediante la acción individual de arriba: las mismas reglas, eventos de auditoría,
notificaciones y fusión automática. `note` es obligatorio en `request_changes` y `decline` (`422` sin
él) y opcional en `approve`; se deja en cada solicitud, como nota de la decisión del revisor o como
`declineNote`.

```json
{
  "done": [{ "reviewUlid": "01J8Z3W4X5Y6Z7A8B9C0D1E2F5", "displayId": "RV-3", "status": "open" }],
  "skipped": [
    {
      "reviewUlid": "01J8Z3W4X5Y6Z7A8B9C0D1E2F8",
      "httpStatus": 403,
      "reason": null,
      "message": "the author cannot review their own request"
    }
  ]
}
```

`done` lista las solicitudes sobre las que se aplicó la decisión, con su `status` después de ella
(`merged` cuando una aprobación la fusionó automáticamente) y `autoMergeFailure` (como en una
aprobación individual). Una solicitud que la acción individual
rechazaría aparece en `skipped` con el `httpStatus`, el `details.reason` y el mensaje de ese
rechazo; las demás se procesan igual.

## Comentarios en una solicitud

```http
GET    /api/v1/projects/{projectId}/test-case-reviews/{reviewUlid}/comments
POST   /api/v1/projects/{projectId}/test-case-reviews/{reviewUlid}/comments
PATCH  /api/v1/projects/{projectId}/test-case-reviews/{reviewUlid}/comments/{commentUlid}
DELETE /api/v1/projects/{projectId}/test-case-reviews/{reviewUlid}/comments/{commentUlid}
POST   /api/v1/projects/{projectId}/test-case-reviews/{reviewUlid}/comments/{commentUlid}/pin
DELETE /api/v1/projects/{projectId}/test-case-reviews/{reviewUlid}/comments/{commentUlid}/pin
```

La conversación de una solicitud usa los mismos cuerpos, respuestas y reglas que los
[comentarios de casos de prueba](https://docs.probara.net/es/developers/api/test-case-comments/): respuestas, menciones
`@[userUlid]`, imágenes adjuntas y comentarios fijados. Cualquiera que pueda leer el proyecto lista
los comentarios; los miembros y roles superiores los publican, editan, eliminan y fijan, tanto en
solicitudes abiertas como cerradas. Una solicitud de otro proyecto responde `404`, y la restricción
del plan responde `403` como en el resto de endpoints de revisión.

Las entradas de actividad son `test_case_review.commented`, `test_case_review.comment_updated`,
`test_case_review.comment_deleted`, `test_case_review.comment_pinned` y
`test_case_review.comment_unpinned`.

## Cuando se elimina un caso

Eliminar un caso (`DELETE /test-cases/{caseUlid}`, o fusionar una solicitud `delete`) conserva sus
solicitudes de revisión y sus comentarios. Su solicitud abierta se rechaza con `declineNote`
`"The test case was deleted."` (una entrada `test_case_review.declined` con `"caseDeleted": true`, y
el aviso `decided`). Después, cada solicitud sobre el caso tiene `caseUlid` y `case` en `null` y
`deletedCase` informado.

## Notificaciones

Las acciones de revisión avisan a las personas involucradas, con notificaciones cuyo `entityType` es
`test_case_review` (consulta la [API de notificaciones](https://docs.probara.net/es/developers/api/notifications/#tipos)):

| `kind`                             | Se envía a                                                             | Al                                                                         |
| ---------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `test_case_review.assigned`        | Los revisores que añade un envío o un cambio de revisores              | `POST /test-case-reviews`, `PUT …/reviewers`                               |
| `test_case_review.decided`         | Los participantes                                                      | `approve`, `request-changes`, `decline`, `merge` (y una fusión automática) |
| `test_case_review.edited`          | Los participantes                                                      | `PATCH …/{reviewUlid}`                                                     |
| `test_case_review.comment_created` | Los participantes, salvo los que menciona el comentario                | `POST …/comments`                                                          |
| `test_case_review.comment_mention` | Los miembros que menciona un comentario, o que añade la edición de uno | `POST …/comments`, `PATCH …/comments/{commentUlid}`                        |

Los participantes son el autor de la solicitud, sus revisores y todos los que comentaron en ella.
Quien hace la acción nunca recibe aviso de ella, y solo se avisa a los miembros que todavía pueden
leer el proyecto. Las acciones de un token de API avisan a las mismas personas.

Una acción masiva sobre dos o más solicitudes envía un solo aviso por destinatario para todo el lote,
con `entityType` `test_case_review_batch` y `eventCount` igual al número de solicitudes del lote que
le conciernen: `test_case_review.assigned` para `POST .../bulk` (que enlaza a
`/projects/{code}/review?status=assigned`) y `test_case_review.decided` para
`POST .../bulk-decision` (con la decisión en `payload.context.status`, que enlaza a la lista de
Revisión).

## Revisión obligatoria

Mientras la revisión del proyecto sea **obligatoria**, `POST .../test-cases` y el
`PATCH /test-cases/{caseUlid}` consolidado abren una solicitud con las reglas de este endpoint y
responden `202` con ella (se asignan los revisores por defecto del proyecto), y una importación abre
una solicitud por caso. `POST /test-cases/{caseUlid}/archive`, `DELETE /test-cases/{caseUlid}` y
`POST .../test-cases/bulk-archive` abren solicitudes `archive` y `delete` de la misma manera. Las demás escrituras directas de casos responden `409 conflict` con
`details.reason` `review_required` y no escriben nada. Consulta
[Revisión obligatoria de casos de prueba](https://docs.probara.net/es/developers/api-reference/#revisión-obligatoria-de-casos-de-prueba-202-y-review_required).

## MCP

Cada endpoint de esta página tiene una herramienta MCP, con las mismas reglas y rechazos:

| Herramienta                          | Endpoint                                                                          |
| ------------------------------------ | --------------------------------------------------------------------------------- |
| `create_test_case_review`            | `POST .../test-case-reviews` (`kind` y después `case`, o `case_ulid` y `changes`) |
| `get_test_case_review`               | `GET .../{reviewUlid}`                                                            |
| `get_test_case_review_by_display_id` | `GET .../by-display-id/{displayId}`                                               |
| `list_test_case_reviews`             | `GET .../test-case-reviews`                                                       |
| `get_test_case_review_versions`      | `GET .../{reviewUlid}/versions`                                                   |
| `approve_test_case_review`           | `POST .../{reviewUlid}/approve`                                                   |
| `unapprove_test_case_review`         | `POST .../{reviewUlid}/unapprove`                                                 |
| `request_changes_test_case_review`   | `POST .../{reviewUlid}/request-changes`                                           |
| `update_test_case_review`            | `PATCH .../{reviewUlid}`                                                          |
| `set_test_case_review_reviewers`     | `PUT .../{reviewUlid}/reviewers`                                                  |
| `decline_test_case_review`           | `POST .../{reviewUlid}/decline`                                                   |
| `merge_test_case_review`             | `POST .../{reviewUlid}/merge`                                                     |
| `bulk_create_test_case_reviews`      | `POST .../test-case-reviews/bulk`                                                 |
| `bulk_decide_test_case_reviews`      | `POST .../test-case-reviews/bulk-decision`                                        |

Con la revisión obligatoria, `create_test_case`, `update_test_case`, `archive_test_case` y
`delete_test_case` devuelven `{ "sentToReview": true, "message": "…", "review": { … } }` en lugar de
actuar, y `bulk_archive_test_cases` devuelve `{ "sentToReview": true, "message": "…", "created",
"skipped" }`. Consulta
[Herramientas MCP](https://docs.probara.net/es/developers/mcp-tools/).
