# Adjuntos de resultado

> Adjunta capturas, trazas y logs a un resultado de prueba registrado.

Un resultado (un intento de un caso de ejecución) puede llevar hasta **20 archivos** de casi cualquier tipo: capturas de pantalla, trazas de Playwright, vídeos, logs. Súbelos en dos pasos: **prepara** los archivos y luego **confirma** la lista de adjuntos del resultado. Un adjunto pertenece al resultado completo o, con `stepIndex`, a uno de los pasos que un reporte de CI envió con él ([Adjuntar a un paso reportado](#adjuntar-a-un-paso-reportado)); para las imágenes que un tester añade a un paso del caso mientras ejecuta, consulta [Adjuntos de resultado de ejecución](https://docs.probara.net/es/developers/api/run-result-attachments/).

El acceso requiere `member`+ en el proyecto de la ejecución; un `viewer` recibe `403 forbidden`. Las escrituras siguen la misma regla de ejecución cerrada que el resto de escrituras de resultados: se permiten en una ejecución `open`, y en una ejecución `closed` no abortada mientras la [configuración de ejecución](https://docs.probara.net/es/developers/api/project-run-settings/) `allowResultsInClosedRuns` del proyecto resuelva a `true` (el valor por defecto). Una ejecución `aborted` siempre responde `409 conflict`. Cuando el proyecto reserva los resultados al asignado (`assigneeResultLock`), quien no es el tester asignado recibe `403 run_case_assignee_locked`; los tokens de API nunca están exentos.

## Flujo

1. **Obtén el resultado.** Un reporte de CI contesta cada entrada emparejada con su `resultUlid` (`results[i].resultUlid` de `POST /api/v1/projects/{projectId}/reports`). `GET /api/v1/runs/{runUlid}/results` lista todos los resultados de una ejecución.
2. **Preparar** — `POST /api/v1/runs/{runUlid}/results/{resultUlid}/attachments:stage` con `multipart/form-data` y una o más partes `file`. Devuelve una referencia preparada por archivo. Todavía no se adjunta nada ni se cobra almacenamiento.
3. **Confirmar** — `PATCH /api/v1/runs/{runUlid}/results/{resultUlid}/attachments` con la lista completa del resultado, cada elemento con su `position`. Devuelve la lista confirmada.
4. **Leer** — `GET /api/v1/runs/{runUlid}/results/{resultUlid}` devuelve `resultAttachments`, ordenados por `position`: los archivos confirmados y las imágenes de los textos del resultado (ver la sección Imágenes en el texto).

Un archivo preparado que nunca se confirma caduca por sí solo (unas 48 horas). Por eso reintentar una preparación es seguro; las peticiones de preparación ignoran la cabecera [`Idempotency-Key`](https://docs.probara.net/es/developers/api/idempotency/). La confirmación y el borrado sí la aceptan.

## Preparar (multipart)

Mantén el cuerpo completo de la petición en 64 MiB como máximo (`ATTACHMENT_UPLOAD_REQUEST_RECOMMENDED_MAX_BYTES`) y reparte los lotes más grandes en varias peticiones. Cloudflare rechaza un cuerpo de más de 100 MB con `413` antes de que llegue a la API, permitan lo que permitan los límites por archivo y por petición; ese `413` no lleva el envoltorio de error de la API.

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -F "file=@screenshot.png" \
  -F "file=@trace.zip" \
  "https://app.probara.net/api/v1/runs/$RUN/results/$RESULT/attachments:stage"
```

```json
{
  "attachments": [
    {
      "ulid": "01J...ATT1",
      "objectKey": "staging/01J...ORG/01J...UPLOAD1.webp",
      "thumbKey": "staging/01J...ORG/01J...UPLOAD1_thumb.webp",
      "mime": "image/webp",
      "byteSize": 8420,
      "originalFilename": "screenshot.png",
      "disposition": "inline",
      "width": 1280,
      "height": 720
    },
    {
      "ulid": "01J...ATT2",
      "objectKey": "staging/01J...ORG/01J...UPLOAD2.zip",
      "thumbKey": null,
      "mime": "application/zip",
      "byteSize": 1048576,
      "originalFilename": "trace.zip",
      "disposition": "attachment",
      "width": null,
      "height": null
    }
  ]
}
```

Límites:

- Hasta **20** partes `file` por petición. Los archivos vacíos se rechazan.
- Cualquier tipo salvo ejecutables y scripts (`application/x-msdownload`, `application/x-msdos-program`, `application/x-sh`, `application/x-bat`, `application/x-msi`, `application/x-executable`, `application/vnd.microsoft.portable-executable`), comprobado contra el tipo de contenido declarado de cada parte.
- Las imágenes PNG, JPEG y WebP (según el tipo de contenido declarado) se convierten a WebP con miniatura y vuelven con `disposition: "inline"`. Tienen un límite de **10 MiB** y **8192 px** por lado.
- Cualquier otro archivo se guarda tal cual, hasta **32 MiB**, con `disposition: "attachment"`.

La primera parte inválida rechaza **la petición entera** con `422 validation_failed`: no vuelve ninguna referencia, ni siquiera de las partes anteriores (esas caducan como cualquier otro archivo preparado sin referenciar). El `message` del error es `empty_file`, `denied_type` o `file_too_large`, que también cubre una imagen que supera el límite de píxeles o cuya cabecera no se puede leer. Valida los archivos antes de subirlos, o reintenta los archivos restantes de uno en uno por petición para que un archivo malo no retenga a los demás.

### Antes de que exista el resultado

Un cliente que sube evidencias antes de registrar el resultado puede preparar contra el caso de ejecución: `POST /api/v1/runs/{runUlid}/cases/{runCaseUlid}/result-attachments:stage`. Acepta el mismo cuerpo, tiene los mismos límites y devuelve las mismas referencias. Rechaza cualquier ejecución cerrada con `409 conflict`, sea cual sea la configuración. Confirma las referencias en el resultado cuando exista.

## Confirmar (reemplaza la lista)

El cuerpo es la lista **completa** de adjuntos del resultado. Los archivos confirmados que falten se borran, así que reenvía cada archivo existente como `{ ulid, position }` para conservarlo. Un elemento nuevo es una referencia preparada más su `position`.

```json
{
  "attachments": [
    { "ulid": "01J...KEEP", "position": 0 },
    {
      "ulid": "01J...ATT2",
      "position": 1,
      "objectKey": "staging/01J...ORG/01J...UPLOAD2.zip",
      "mime": "application/zip",
      "byteSize": 1048576,
      "originalFilename": "trace.zip",
      "disposition": "attachment",
      "thumbKey": null,
      "width": null,
      "height": null
    }
  ]
}
```

- Un elemento nuevo necesita al menos `ulid`, `position`, `objectKey` y `originalFilename`; lo más sencillo es enviar la referencia preparada tal cual más `position`.
- Un resultado admite como máximo **20** adjuntos. La confirmación es todo o nada: una confirmación rechazada no cambia nada.
- `{ "attachments": [] }` elimina todos los adjuntos del resultado.
- Un elemento puede llevar `stepIndex` para adjuntar el archivo a un paso reportado; ver abajo.
- Un [token de aplicación](https://docs.probara.net/es/developers/authentication/#tokens-de-aplicación) solo puede añadir: un cuerpo que deja fuera un adjunto que el resultado ya tiene responde `403 forbidden` y no cambia nada. Un token de aplicación no puede preparar contra el caso de ejecución ni eliminar un adjunto.
- Los bytes confirmados cuentan para la cuota de almacenamiento de la organización (categoría `result_attachment`); los preparados no. Una confirmación cuyo crecimiento superaría la cuota responde `413 storage_quota_exceeded`.

### Adjuntar a un paso reportado

Un resultado registrado por un [reporte de CI](https://docs.probara.net/es/developers/api-reference/#reportes-de-ci) puede
llevar los pasos que ejecutó (los `steps` de la entrada). Para adjuntar un archivo a uno de ellos,
añade al elemento `stepIndex`: el índice del paso en un recorrido en profundidad y en preorden de los
pasos tal como se enviaron, desde 0 — la `position` del paso en `resultSteps` de la
[instantánea del intento](https://docs.probara.net/es/developers/api/run-result-snapshots/).

```json
{
  "attachments": [
    { "ulid": "01J...KEEP", "position": 0 },
    {
      "ulid": "01J...SHOT",
      "position": 1,
      "stepIndex": 3,
      "objectKey": "staging/01J...ORG/01J...UPLOAD3.webp",
      "originalFilename": "pay.png"
    }
  ]
}
```

| `stepIndex` en el elemento | Elemento nuevo          | Adjunto existente       |
| -------------------------- | ----------------------- | ----------------------- |
| Omitido                    | Se adjunta al resultado | Conserva su paso actual |
| `null`                     | Se adjunta al resultado | Vuelve al resultado     |
| Un número                  | Se adjunta a ese paso   | Se mueve a ese paso     |

- El índice debe nombrar un paso de **este** resultado. Si no, toda la confirmación responde
  `422 validation_failed` y no se escribe nada.
- Los adjuntos de pasos cuentan para el mismo límite de **20** archivos por resultado.
- La respuesta de la confirmación, `resultAttachments` de la instantánea del intento y el detalle del
  caso de la ejecución devuelven el `stepIndex` de cada adjunto (`null` para un archivo del
  resultado). Reenvíalo tal cual al confirmar de nuevo la lista, u omítelo para conservarlo.

## Borrar un adjunto

```bash
curl -sS -X DELETE \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/runs/$RUN/results/$RESULT/attachments/$ATT"
```

`204` si tiene éxito. Borrar un adjunto que ya no existe también responde `204`.

## Errores

| Estado | Código                      | Cuándo                                                                                                                                                                                                                                                                                                                                                            |
| ------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 403    | `forbidden`                 | Rol `viewer`; o un token de aplicación cuya confirmación eliminaría un adjunto, o que llama a una operación distinta de preparar y confirmar a nivel de resultado                                                                                                                                                                                                 |
| 403    | `run_case_assignee_locked`  | El proyecto reserva los resultados al asignado y quien llama no es el tester asignado (operaciones sobre el resultado)                                                                                                                                                                                                                                            |
| 403    | `project_locked`            | El proyecto está bloqueado                                                                                                                                                                                                                                                                                                                                        |
| 404    | `not_found`                 | Ejecución, resultado o caso de ejecución desconocido, o de otra organización                                                                                                                                                                                                                                                                                      |
| 409    | `conflict`                  | La ejecución está abortada, o cerrada con `allowResultsInClosedRuns` a `false`; la preparación sobre el caso rechaza toda ejecución cerrada                                                                                                                                                                                                                       |
| 409    | `staging_already_committed` | Solo confirmación: una referencia preparada ya se confirmó                                                                                                                                                                                                                                                                                                        |
| 409    | `project_archived`          | El proyecto está archivado                                                                                                                                                                                                                                                                                                                                        |
| 413    | —                           | Solo preparación: el cuerpo de la petición supera los 100 MB; Cloudflare lo rechaza antes de que llegue a la API, sin el envoltorio de error                                                                                                                                                                                                                      |
| 413    | `storage_quota_exceeded`    | Solo confirmación: se superaría la cuota de almacenamiento de la organización                                                                                                                                                                                                                                                                                     |
| 415    | `unsupported_media_type`    | Solo confirmación: el cuerpo no es `application/json`                                                                                                                                                                                                                                                                                                             |
| 422    | `validation_failed`         | Preparación: sin parte `file`, demasiados archivos, un archivo vacío o demasiado grande, una imagen que supera el límite de píxeles, o un tipo denegado; una parte inválida rechaza la petición entera. Confirmación: cuerpo inválido, más de 20 adjuntos, una referencia preparada inválida o caducada, o un `stepIndex` que no nombra ningún paso del resultado |
| 429    | `too_many_requests`         | El límite de peticiones de API de la organización; espera `Retry-After` segundos                                                                                                                                                                                                                                                                                  |

## Imágenes en el texto

La lista `resultAttachments` también incluye las imágenes insertadas en los textos del resultado, junto con sus archivos. Cada imagen tiene `kind` `image` y los campos de una subida (`ulid`, `mime`, `width`, `height`, `byteSize`, `createdAt`), más `field`, que indica en qué texto está:

- `{ "kind": "description" }`, `{ "kind": "precondition" }` o `{ "kind": "postcondition" }` para los textos del caso que el intento congeló con su encabezado.
- `{ "kind": "step_actual_result", "runCaseStepUlid": "01J...", "position": 2 }` para el resultado real de un paso.
- `{ "kind": "notes" }` para las notas del resultado.

Los archivos conservan `kind` `file`. Una imagen sale de la lista cuando sale de su texto.

## Relacionado

- [Resultados reales de los pasos](https://docs.probara.net/es/developers/api/run-result-attachments/) — el texto y las imágenes insertadas del resultado de un paso
- [Configuración de ejecución](https://docs.probara.net/es/developers/api/project-run-settings/) — `allowResultsInClosedRuns` y `assigneeResultLock`
- [Referencia interactiva v1](https://docs.probara.net/es/developers/api-reference/v1/) — esquemas completos
