API de comentarios en casos de prueba
Los comentarios de casos de prueba se adjuntan a un caso específico mediante un almacén genérico de comentarios (entity_type='test_case', entity_id), compartido con los comentarios de defectos. Ambas superficies son estructuralmente idénticas y solo difieren en la ruta de la entidad. Los comentarios se limitan a la organización mediante el ULID del caso de prueba.
Los comentarios admiten un nivel de respuestas en hilo, fijado y resolución de menciones @. El cuerpo es texto enriquecido: envíalo como Markdown en body, o como documento en bodyDocument. Las imágenes van insertadas en el cuerpo, preparadas con POST /api/v1/images:stage; un comentario no tiene lista de adjuntos.
Endpoints
Sección titulada «Endpoints»| Método | Ruta | Descripción |
|---|---|---|
GET | /api/v1/test-cases/{caseUlid}/comments | Listar comentarios de primer nivel (paginación por cursor) |
POST | /api/v1/test-cases/{caseUlid}/comments | Agregar un comentario, o una respuesta usando parentUlid |
PATCH | /api/v1/test-cases/{caseUlid}/comments/{commentUlid} | Editar un comentario o respuesta |
DELETE | /api/v1/test-cases/{caseUlid}/comments/{commentUlid} | Eliminar un comentario (elimina en cascada sus respuestas) |
POST | /api/v1/test-cases/{caseUlid}/comments/{commentUlid}/pin | Fijar un comentario de primer nivel |
DELETE | /api/v1/test-cases/{caseUlid}/comments/{commentUlid}/pin | Desfijar un comentario de primer nivel |
{caseUlid} debe pertenecer a la organización activa. El acceso cruzado entre organizaciones devuelve 404 not_found.
Autorización
Sección titulada «Autorización»Todos los endpoints requieren una sesión autenticada o un token Bearer con al menos acceso viewer.
| Operación | Rol mínimo |
|---|---|
GET | Visualizador |
POST (comentario o respuesta) | Miembro |
PATCH | Miembro (propio comentario únicamente) |
DELETE | Miembro (propio) o Administrador/Propietario (cualquiera) |
POST / DELETE .../pin | Miembro (cualquier comentario de primer nivel) |
GET — listar comentarios
Sección titulada «GET — listar comentarios»/api/v1/test-cases/{caseUlid}/commentsDevuelve los comentarios de primer nivel (parentUlid: null) con paginación por cursor.
Parámetros de consulta
Sección titulada «Parámetros de consulta»| Parámetro | Tipo | Descripción |
|---|---|---|
cursor | string | Cursor ULID de la página anterior (nextCursor). |
limit | integer | Tamaño de página. Predeterminado 50, máximo 200. |
sort | oldest | newest | Orden de los comentarios de primer nivel. Predeterminado oldest (sin cambios respecto al comportamiento público previo). Las respuestas siempre se mantienen en orden cronológico bajo su padre, sin importar este valor. |
Respuesta (200 OK)
Sección titulada «Respuesta (200 OK)»{ "items": [ { "ulid": "01J…", "body": "Reproducido en staging con una sesión nueva del navegador.", "bodyDocument": { "type": "doc", "attrs": { "version": 1 }, "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Reproducido en staging con una sesión nueva del navegador." } ] } ] }, "author": { "ulid": "01J…", "displayName": "Ada Lovelace", "avatarKey": null, "avatarVersion": 0 }, "createdAt": 1700000000000, "editedAt": null, "attachments": [], "parentUlid": null, "pinnedAt": null, "pinnedBy": null, "mentions": [], "replyCount": 1, "replies": [ { "ulid": "01J…RPLY", "body": "Confirmado — mismo resultado de mi lado. @[01J…GRACE]", "bodyDocument": { "type": "doc", "attrs": { "version": 1 }, "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Confirmado — mismo resultado de mi lado. " }, { "type": "mention", "attrs": { "id": "01J…GRACE" } } ] } ] }, "author": { "ulid": "01J…", "displayName": "Grace Hopper", "avatarKey": null, "avatarVersion": 0 }, "createdAt": 1700000100000, "editedAt": null, "attachments": [], "parentUlid": "01J…", "pinnedAt": null, "pinnedBy": null, "mentions": [{ "ulid": "01J…GRACE", "displayName": "Grace Hopper" }] } ] } ], "nextCursor": "01J…", "pinned": []}body(Markdown) ybodyDocument(el documento) contienen el mismo texto. Al editar, devuelve uno de los dos, nunca ambos.attachmentssiempre es[]. Las imágenes de un comentario van insertadas en su cuerpo.authoresnullcuando el miembro fue removido de la organización.editedAtesnullcuando el comentario nunca se ha editado.nextCursoresnullen la última página.pinnedes el conjunto completo de comentarios de primer nivel fijados para este caso de prueba, ordenado porpinnedAtascendente, independiente del cursor paginado deitems— muéstralo encima del feed. Es idéntico sin importarsort.- Un objeto de respuesta nunca lleva
replyCountnireplies— la profundidad del hilo está limitada a un nivel. - El arreglo
repliesde un elemento de primer nivel está en orden cronológico ascendente y limitado a 100 entradas inline.
Importante: las respuestas no aparecen en la enumeración plana de items[] — solo son accesibles a través de replies[] de su comentario padre. Este es un cambio aditivo pero no compatible byte a byte con los clientes que asumían que items[] enumeraba todos los comentarios: recorre replies de cada elemento si necesitas el contenido de las respuestas, y usa replyCount si solo necesitas el conteo.
POST — agregar un comentario o respuesta
Sección titulada «POST — agregar un comentario o respuesta»/api/v1/test-cases/{caseUlid}/commentsCuerpo de la solicitud
Sección titulada «Cuerpo de la solicitud»| Campo | Requerido | Notas |
|---|---|---|
body | uno de los dos | Markdown. Sin espacios sobrantes, máximo 5000 caracteres, y debe contener contenido: los valores que solo contienen espacios son rechazados. Puede contener tokens de mención @[ulid] (ver Menciones). |
bodyDocument | uno de los dos | El documento versionado del cuerpo (ver Campos de texto enriquecido). Envía body o bodyDocument, nunca ambos. |
parentUlid | no | ULID de un comentario de primer nivel existente en el mismo caso de prueba. Crea una respuesta de profundidad 1. Omítelo para crear un comentario de primer nivel (comportamiento sin cambios). |
Errores:
422 validation_failed— se envíanbodyybodyDocumenta la vez, ninguno contiene contenido,bodysupera los 5000 caracteres, la solicitud incluye una clave que este endpoint no acepta (comoattachments), oparentUlidapunta a un comentario que es en sí mismo una respuesta (responder a una respuesta está rechazado). El mensaje describe el problema;detailscontiene el campo que falló.422 validation_failedcondetails.fieldigual abody— un rechazo de texto enriquecido.details.codeindica la regla que rompe el cuerpo, por ejemplotoo_largepara un documento que supera el límite de 256 KiB (ver Campos de texto enriquecido).422 validation_failedcondetails.codeigual aunknown_image— el documento menciona una imagen que nunca se preparó o que expiró.details.ulidsenumera esas imágenes.404 not_found—parentUlidno corresponde a un comentario de primer nivel de este caso de prueba (entidad u organización incorrecta).403— rol visualizador.
Respuesta (201 Created)
Sección titulada «Respuesta (201 Created)»{ "ulid": "01J…", "body": "También se ve bien en móvil.", "bodyDocument": { "type": "doc", "attrs": { "version": 1 }, "content": [ { "type": "paragraph", "content": [{ "type": "text", "text": "También se ve bien en móvil." }] } ] }, "author": { "ulid": "01J…", "displayName": "Ada", "avatarKey": null, "avatarVersion": 0 }, "createdAt": 1700000000000, "editedAt": null, "attachments": [], "parentUlid": null, "pinnedAt": null, "pinnedBy": null, "mentions": [], "replyCount": 0, "replies": []}Auditoría: emite test_case.commented con metadata.excerpt (como máximo 140 caracteres del texto plano del cuerpo).
PATCH — editar un comentario
Sección titulada «PATCH — editar un comentario»/api/v1/test-cases/{caseUlid}/comments/{commentUlid}Solo el autor original del comentario puede editarlo (exclusivo del autor, ya sea de primer nivel o respuesta). Los administradores y propietarios no pueden editar el comentario de otro miembro. Las menciones se vuelven a resolver a partir del nuevo cuerpo en cada edición.
Cuerpo de la solicitud
Sección titulada «Cuerpo de la solicitud»| Campo | Requerido | Notas |
|---|---|---|
body | uno de los dos | Cuerpo Markdown de reemplazo, con la misma validación que POST. |
bodyDocument | uno de los dos | Documento de reemplazo, con la misma validación que POST. Envía body o bodyDocument, nunca ambos. |
Respuesta (200 OK)
Sección titulada «Respuesta (200 OK)»Comentario actualizado con editedAt establecido en el momento actual y mentions[] reflejando el nuevo cuerpo.
Errores: 403 si no eres el autor. 404 si el comentario no existe en este caso (caseUlid o organización incorrectos). 422 validation_failed para los mismos rechazos que POST.
Auditoría: emite test_case.comment_updated con metadata.excerpt.
DELETE — eliminar un comentario
Sección titulada «DELETE — eliminar un comentario»/api/v1/test-cases/{caseUlid}/comments/{commentUlid}Los autores pueden eliminar su propio comentario. Los administradores y propietarios pueden eliminar cualquier comentario. Eliminar un comentario de primer nivel elimina en cascada todas sus respuestas. Cada comentario eliminado libera las imágenes insertadas que contiene su cuerpo, de modo que una imagen que ningún otro texto ni revisión almacenada usa se elimina junto con él.
Respuesta (204 No Content)
Sección titulada «Respuesta (204 No Content)»Errores: 403 si el miembro intenta eliminar el comentario de otro miembro.
Auditoría: emite test_case.comment_deleted por el comentario eliminado, más un test_case.comment_deleted adicional por cada respuesta eliminada en cascada.
POST — fijar un comentario
Sección titulada «POST — fijar un comentario»/api/v1/test-cases/{caseUlid}/comments/{commentUlid}/pinFija un comentario de primer nivel para que siempre aparezca en el arreglo pinned[] de la respuesta de listado, mostrado antes del feed paginado sin importar sort. Cualquier miembro o rol superior puede fijar cualquier comentario de primer nivel (no solo los propios). No hay límite en la cantidad de comentarios fijados por caso de prueba.
Errores: 422 validation_failed si commentUlid corresponde a una respuesta — solo los comentarios de primer nivel pueden fijarse. 403 para el rol visualizador. 404 si el comentario no existe en este caso.
Respuesta (200 OK)
Sección titulada «Respuesta (200 OK)»Comentario actualizado con pinnedAt (epoch ms) y pinnedBy (resumen del actor que lo fijó) establecidos.
Auditoría: emite test_case.comment_pinned.
DELETE — desfijar un comentario
Sección titulada «DELETE — desfijar un comentario»/api/v1/test-cases/{caseUlid}/comments/{commentUlid}/pinLimpia pinnedAt/pinnedBy. Cualquier miembro o rol superior puede desfijar.
Respuesta (200 OK)
Sección titulada «Respuesta (200 OK)»Comentario actualizado con pinnedAt: null, pinnedBy: null.
Errores: 403 para el rol visualizador. 404 si el comentario no existe en este caso.
Auditoría: emite test_case.comment_unpinned.
Menciones
Sección titulada «Menciones»El cuerpo de un comentario puede incluir tokens @[ulid] que referencian a un miembro de la organización por su ULID (la forma ULID, no un nombre visible, para que los cambios de nombre nunca rompan una mención). En bodyDocument, la misma mención es un nodo mention cuyo attrs.id es ese ULID. Al crear o editar, el servidor:
- Extrae cada mención del cuerpo: el token
@[ulid]en Markdown o el nodomentionen el documento. - Resuelve cada mención solo contra los miembros de la organización activa — un ULID de otra organización, o uno que no corresponde a un miembro, se deja como texto plano sin formato y no crea ninguna fila de mención.
- Persiste una fila por cada mención válida distinta, y reconcilia el conjunto en cada edición (las menciones eliminadas se borran y se agregan las nuevas).
El arreglo mentions[] de la respuesta siempre refleja el nombre visible actual del miembro mencionado — renombrar a un miembro actualiza cada mención pasada que lo referencia. Las menciones nunca generan una notificación ni un correo.
Eliminación en cascada
Sección titulada «Eliminación en cascada»Cuando se elimina un caso de prueba (DELETE /api/v1/test-cases/{caseUlid}), todos sus comentarios y respuestas se eliminan junto con las imágenes insertadas que contienen, en la misma transacción que el caso. No se requiere ninguna llamada adicional a la API; si la eliminación se rechaza, los comentarios se conservan.
Ejemplos
Sección titulada «Ejemplos»# Listar la página de comentarios más recientes primerocurl -sS \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://app.probara.net/api/v1/test-cases/01J...CASE/comments?sort=newest"
# Agregar un comentario de primer nivelcurl -sS -X POST \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"body":"Verificado en Safari 17 — pasa."}' \ "https://app.probara.net/api/v1/test-cases/01J...CASE/comments"
# Responder a un comentario de primer nivel, mencionando a un compañerocurl -sS -X POST \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"body":"Gracias @[01J...GRACE] — confirmado también aquí.","parentUlid":"01J...CMT"}' \ "https://app.probara.net/api/v1/test-cases/01J...CASE/comments"
# Fijar un comentariocurl -sS -X POST \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://app.probara.net/api/v1/test-cases/01J...CASE/comments/01J...CMT/pin"
# Desfijar un comentariocurl -sS -X DELETE \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://app.probara.net/api/v1/test-cases/01J...CASE/comments/01J...CMT/pin"
# Editar un comentariocurl -sS -X PATCH \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"body":"Nota actualizada tras volver a probar."}' \ "https://app.probara.net/api/v1/test-cases/01J...CASE/comments/01J...CMT"
# Eliminar un comentariocurl -sS -X DELETE \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://app.probara.net/api/v1/test-cases/01J...CASE/comments/01J...CMT"