Ir al contenido

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).

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.

{
"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 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).

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

Estadodetails.reasonCuá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.
409review_not_enabledEl proyecto no tiene la revisión activada.
409review_already_openEl caso ya tiene una solicitud abierta. details.reviewUlid y details.displayId la identifican.
409stale_revisionchanges.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.
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
}
CampoSignificado
displayIdRV-{n}, numerado por proyecto.
kindcreate (un caso nuevo), update (un cambio en caseUlid), archive o delete (archivar o eliminar caseUlid).
statusopen, merged o declined.
titleEl título del caso tal como lo deja la propuesta.
baseRevisionLa revisión del caso sobre la que se propuso un update, archive o delete; null en create.
proposalVersionEmpieza en 1 y sube cada vez que se edita la propuesta.
proposalEl cuerpo que se aplica al fusionar la solicitud. Las imágenes preparadas apuntan a las copias que conserva.
reviewersLa decision de cada revisor (pending, approved, changes_requested) y la versión sobre la que la tomó.
caseEl caso que nombra caseUlid (su displayId y su título actual); null en un create sin fusionar y cuando el caso se eliminó.
deletedCaseEl displayId y el title del caso cuando se eliminó (al fusionar un delete o directamente); null mientras existe. caseUlid es entonces null.
approvalCountLas aprobaciones sobre la proposalVersion actual. La aprobación del propio autor nunca cuenta.
approvalsRequiredLas aprobaciones que el proyecto exige antes de poder fusionar la solicitud (0: ninguna).
allowSelfMergeSi el proyecto permite que el autor fusione su propia solicitud.
conflicttrue 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).
currentVersionOnlytrue 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.
declineNoteLa nota que se dejó al rechazar la solicitud; null en otro caso.
declineReasonPor 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ó.

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ámetroSignificado
statusopen (por defecto), merged, declined o all.
kindSolo solicitudes de un tipo (create, update, archive, delete).
authorSolo las solicitudes que envió este miembro (ULID de usuario).
reviewerSolo las solicitudes que revisa este miembro (ULID de usuario).
assignedToMetrue: solo las solicitudes que revisas tú.
needsMyReviewtrue: solo las solicitudes abiertas que esperan tu decisión.
pagePá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.

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.

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.

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ónQuién
POST .../{reviewUlid}/approveCualquier miembro cuyo rol tenga test-cases.review, salvo el autor.
POST .../{reviewUlid}/unapproveCualquier miembro cuyo rol tenga test-cases.review, sobre su propia aprobación de la propuesta actual (si no, 409 not_approved).
POST .../{reviewUlid}/request-changesCualquier 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}/reviewersEl autor, o cualquier miembro cuyo rol tenga test-cases.review.
POST .../{reviewUlid}/declineEl autor, o cualquier miembro cuyo rol tenga test-cases.review.
POST .../{reviewUlid}/mergeCualquier 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.

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).

{
"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.

{ "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.

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.

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.

Estadodetails.reasonCuá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.
409review_closedLa solicitud ya está fusionada o rechazada (details.status).
409review_conflictSolo al fusionar: el caso cambió después de hacer la propuesta. Edita la propuesta y luego fusiónala.
409approvals_requiredSolo 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.
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.

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: 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.

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.

Las acciones de revisión avisan a las personas involucradas, con notificaciones cuyo entityType es test_case_review (consulta la API de notificaciones):

kindSe envía aAl
test_case_review.assignedLos revisores que añade un envío o un cambio de revisoresPOST /test-case-reviews, PUT …/reviewers
test_case_review.decidedLos participantesapprove, request-changes, decline, merge (y una fusión automática)
test_case_review.editedLos participantesPATCH …/{reviewUlid}
test_case_review.comment_createdLos participantes, salvo los que menciona el comentarioPOST …/comments
test_case_review.comment_mentionLos miembros que menciona un comentario, o que añade la edición de unoPOST …/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).

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:

HerramientaEndpoint
create_test_case_reviewPOST .../test-case-reviews (kind y después case, o case_ulid y changes)
get_test_case_reviewGET .../{reviewUlid}
get_test_case_review_by_display_idGET .../by-display-id/{displayId}
list_test_case_reviewsGET .../test-case-reviews
get_test_case_review_versionsGET .../{reviewUlid}/versions
approve_test_case_reviewPOST .../{reviewUlid}/approve
unapprove_test_case_reviewPOST .../{reviewUlid}/unapprove
request_changes_test_case_reviewPOST .../{reviewUlid}/request-changes
update_test_case_reviewPATCH .../{reviewUlid}
set_test_case_review_reviewersPUT .../{reviewUlid}/reviewers
decline_test_case_reviewPOST .../{reviewUlid}/decline
merge_test_case_reviewPOST .../{reviewUlid}/merge
bulk_create_test_case_reviewsPOST .../test-case-reviews/bulk
bulk_decide_test_case_reviewsPOST .../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.