API de solicitudes de revisión de casos
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).
{projectId} es el código del proyecto (por ejemplo ACME).
Enviar un cambio a revisión
Sección titulada «Enviar un cambio a revisión»POST /api/v1/projects/{projectId}/test-case-reviewsRequiere 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.
{ "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.
{ "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.
{ "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 permisotest-cases.review; si no, la solicitud responde422 validation_failedcondetails.reviewerUserIdsenumerando los ULID rechazados, y no se guarda nada.- Las imágenes preparadas con
POST /api/v1/images:stagese 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
suiteUlidy lamilestoneUlidde un caso nuevo deben pertenecer al proyecto, igual que en una creación directa; si no, la solicitud responde422 validation_failedy no se guarda nada. - Cada envío registra una entrada de actividad
test_case_review.createdy avisa a los revisores asignados (consulta Notificaciones).
La respuesta es 201 con la solicitud, tal como la devuelve GET.
Rechazos
Sección titulada «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
Sección titulada «Leer una solicitud de revisión»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:
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.
{ "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. |
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
Sección titulada «Listar solicitudes de revisión»GET /api/v1/projects/{projectId}/test-case-reviews?status=open&page=1&pageSize=50Abierto 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
Sección titulada «Enviar casos a revisión tal como están»POST /api/v1/projects/{projectId}/test-case-reviews/bulk{ "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.
{ "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
Sección titulada «Leer la versión propuesta»GET /api/v1/projects/{projectId}/test-case-reviews/{reviewUlid}/versionsResponde 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
Sección titulada «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
Sección titulada «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
Sección titulada «Editar la propuesta»{ "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
Sección titulada «Cambiar los revisores»{ "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
Sección titulada «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
Sección titulada «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
Sección titulada «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
Sección titulada «Decidir sobre varias solicitudes»POST /api/v1/projects/{projectId}/test-case-reviews/bulk-decision{ "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.
{ "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
Sección titulada «Comentarios en una solicitud»GET /api/v1/projects/{projectId}/test-case-reviews/{reviewUlid}/commentsPOST /api/v1/projects/{projectId}/test-case-reviews/{reviewUlid}/commentsPATCH /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}/pinDELETE /api/v1/projects/{projectId}/test-case-reviews/{reviewUlid}/comments/{commentUlid}/pinLa conversación de una solicitud usa los mismos cuerpos, respuestas y reglas que los
comentarios de casos de prueba: 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
Sección titulada «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
Sección titulada «Notificaciones»Las acciones de revisión avisan a las personas involucradas, con notificaciones cuyo entityType es
test_case_review (consulta la API de notificaciones):
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
Sección titulada «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.
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.