# API de eventos de auditoría

> Lista eventos de auditoría de la organización con paginación por cursor.

Probara expone dos endpoints de solo lectura para listar auditoría:

| Endpoint                                  | Alcance                | Rol mínimo                                          |
| ----------------------------------------- | ---------------------- | --------------------------------------------------- |
| `GET /api/v1/orgs/{orgUlid}/audit-events` | Toda la organización   | Owner o admin                                       |
| `GET /api/v1/runs/{runUlid}/audit-events` | Una ejecución concreta | Cualquier miembro que pueda leer el run (`viewer`+) |

Ambos devuelven el mismo sobre `AuditEventOut` con paginación por cursor. Los eventos se ordenan por ULID descendente (más recientes primero). Los eventos más antiguos que la ventana de retención del historial del plan de la organización se eliminan y dejan de listarse.

## Listado de toda la organización

`GET /api/v1/orgs/{orgUlid}/audit-events` devuelve el registro append-only de auditoría de una organización.

## Autorización

- **Sesión**: inicia sesión en el navegador y envía la cookie de sesión más `X-Organization-Id` si llamas desde scripts propios.
- **Bearer**: token de API con alcance de organización y membresía **owner** o **admin** en esa org.

Los miembros y visualizadores reciben `403 Forbidden`.

## Parámetros de consulta

| Parámetro | Tipo   | Descripción                                                                                     |
| --------- | ------ | ----------------------------------------------------------------------------------------------- |
| `limit`   | entero | Tamaño de página. Por defecto `50`. Valores mayores a `100` se limitan silenciosamente a `100`. |
| `cursor`  | ULID   | Si está presente, devuelve eventos con `ulid < cursor` (página más antigua).                    |

## Forma de la respuesta

```json
{
  "events": [
    {
      "ulid": "01J…",
      "action": "project.created",
      "actor": {
        "kind": "user",
        "ulid": "01J…",
        "email": "alice@example.com",
        "displayName": null,
        "viaApiToken": false
      },
      "entityType": "project",
      "entityUlid": "01J…",
      "entityLabel": "Acme Web",
      "runUlid": null,
      "metadata": {},
      "createdAt": 1700000000000
    }
  ],
  "nextCursor": "01J…"
}
```

`nextCursor` es el ULID del último evento en `events` cuando hay otra página; si no, `null`.

`runUlid` aparece en eventos ligados a un run (y en el listado org-wide cuando el run padre sigue existiendo). Los eventos que no son de run omiten el campo o lo envían como `null`.

## Listado por run

`GET /api/v1/runs/{runUlid}/audit-events` devuelve solo los eventos cuyo `run_id` corresponde al run resuelto dentro de la organización activa. Usa la misma cookie de sesión o token bearer que el resto de APIs de runs; en sesión de navegador envía `X-Organization-Id`.

La autorización coincide con `GET /api/v1/runs/{runUlid}`: cualquier miembro de la organización con acceso al run puede listar su historial. **No** hay restricción owner/admin en esta ruta.

Los parámetros de consulta y el sobre de respuesta son idénticos al endpoint org-wide (`limit`, `cursor`, `events`, `nextCursor`). Un `404` indica que el ULID del run no existe en la organización activa.

Ejemplo:

```bash
export PROBARA_API_TOKEN="probara_…"
export RUN_ULID="01J…"

curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/runs/${RUN_ULID}/audit-events?limit=50"
```

### Variantes de actor

```json
{
  "kind": "user",
  "ulid": "01J…",
  "email": "alice@example.com",
  "displayName": "Alice Anderson",
  "viaApiToken": true
}
```

Una acción hecha con un token de API personal se atribuye a su dueño: el actor es `kind: "user"` con `viaApiToken: true` (`false` en una acción hecha en la app). `kind: "api_token"` es un [token de aplicación](https://docs.probara.net/es/developers/authentication/#tokens-de-aplicación) de la organización, que no pertenece a ninguna persona.

`app` nombra su aplicación de Integraciones — `"junit"` para la aplicación **JUnit XML**, `"playwright"` para la aplicación **Playwright**, `"jest"` para la aplicación **Jest**, `"cypress"` para la aplicación **Cypress**, `"vitest"` para la aplicación **Vitest**, `"cucumber"` para la aplicación **Cucumber** — y solo es `null` cuando el registro del token ya no existe:

```json
{ "kind": "api_token", "ulid": "01J…", "name": "nightly-ci", "app": "junit" }
```

Una acción que tu proveedor de identidad realiza mediante el [aprovisionamiento SCIM](https://docs.probara.net/es/guides/scim-provisioning/) se atribuye al token SCIM que usó, con su ULID y su nombre:

```json
{ "kind": "scim", "ulid": "01J…", "name": "Okta" }
```

```json
{ "kind": "system" }
```

Si un usuario o token fue eliminado, los campos de referencia son `null` pero el evento sigue en la lista:

```json
{ "kind": "user", "ulid": null, "email": null, "displayName": null }
```

## Ejemplo

```bash
export PROBARA_API_TOKEN="probara_…"
export ORG_ULID="01J…"

curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/orgs/${ORG_ULID}/audit-events?limit=50"
```

Página siguiente:

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/orgs/${ORG_ULID}/audit-events?cursor=${NEXT_CURSOR}"
```

## Metadatos y diffs estructurados

`metadata` es un objeto que puede incluir **buckets de diff** compartidos (documentados en OpenAPI como `AuditEventDiff`, `AuditAssigneeChange` y `AuditRunAggregates`) más claves de snapshot propias de cada acción. Los integradores DEBEN tratar claves desconocidas como compatibles hacia adelante.

Buckets de diff habituales:

| Clave            | Usada por                                                                                                                                                                         | Forma                                                                                                                                                                                                                                                                               |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `changes`        | `*.updated`, `run.case_marked`, `run.case_step_marked`, `run.case_retried`, `run.case_step_actual_result_set`, `run.result_attachment_committed` (cuando también cambió el texto) | `AuditFieldChange[]` — `from` / `to` escalares con `fromLabel` / `toLabel` opcionales en referencias                                                                                                                                                                                |
| `assigneeChange` | `run.case_assigned`                                                                                                                                                               | `{ from: { ulid, name } \| null, to: { ulid, name } \| null, via: 'open' \| 'patch' \| 'reconcile' }`                                                                                                                                                                               |
| `bulkCases`      | `run.cases_bulk_*`                                                                                                                                                                | `{ cases: AuditBulkCaseEntry[], omittedCount?, summary: { affected, unchanged, statusFrom?, statusTo?, assigneeFrom?, assigneeTo? } }` — hasta 20 entradas por caso al emitir; el resto en `omittedCount`                                                                           |
| `runAggregates`  | `run.closed`, `run.aborted`                                                                                                                                                       | `{ countPassed, countFailed, countBlocked, countSkipped, countUntested, totalDurationMs, projectedStatus: 'passed' \| 'failed' \| 'aborted' }` — `run.aborted` siempre reporta `projectedStatus: 'aborted'`, aunque los conteos de casos por sí solos leerían `'passed'`/`'failed'` |

Los snapshots de texto mayores a 240 grafemas se recortan al emitir; los objetos afectados marcan `truncated: true` según la convención de auditoría.

### Formatos de `entity.label` en ejecuciones

| `entityType`         | Etiqueta canónica                                                                                           |
| -------------------- | ----------------------------------------------------------------------------------------------------------- |
| `test_run`           | Nombre del run en el momento de emisión                                                                     |
| `test_run_case`      | `${caseDisplayId} · ${titleSnapshot}` — `caseDisplayId` es `${projectCode}-${caseNumber}` (p. ej. `ACME-1`) |
| `test_run_case_step` | ``${caseDisplayId} · ${titleSnapshot} — ${stepActionSnapshot ?? `paso ${stepPosition}`}``                   |

La lista puede añadir metadatos de enlace (`runUlid`, `projectCode`, `caseDisplayId`, `titleSnapshot`, `runName`) cuando la fila guardada no los tenía; los snapshots almacenados siempre prevalecen.

### Metadatos `run.*` por acción

| Acción                            | Claves relevantes en `metadata`                                                                                                                                               |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `run.created`                     | `caseCount`, `environment`, `defaultAssignee`, `milestone` opcionales                                                                                                         |
| `run.updated`                     | `changes` (name, description, defaultAssigneeUlid, environment, milestoneUlid)                                                                                                |
| `run.closed`                      | `runAggregates` (sustituye el `status` plano heredado)                                                                                                                        |
| `run.aborted`                     | `runAggregates` (misma forma que `run.closed`; `projectedStatus` siempre es `'aborted'`)                                                                                      |
| `run.deleted`                     | `runAggregates`, `wasState` (`open` \| `closed`), `closedAt` (null si seguía abierto)                                                                                         |
| `run.cloned`                      | `caseCount`, `sourceRun`, `cloneAssignees`, `statusFilter` (null si se clonaron todos los casos)                                                                              |
| `run.case_added`                  | `added`, `cases` (primeros 20), `omittedCount` opcional                                                                                                                       |
| `run.case_marked`                 | `caseDisplayId`, `titleSnapshot`, `resultUlid`, `changes` (status), `elapsedMs` opcional                                                                                      |
| `run.case_step_marked`            | `caseDisplayId`, `titleSnapshot`, `runCaseUlid`, `stepUlid`, `stepPosition`, `stepActionSnapshot`, `changes` (status), `caseStatus`                                           |
| `run.case_assigned`               | `caseDisplayId`, `titleSnapshot`, `assigneeChange`                                                                                                                            |
| `run.case_retried`                | `caseDisplayId`, `titleSnapshot`, `changes` (status → `untested`), `previousElapsedMs` opcional                                                                               |
| `run.case_removed`                | `caseDisplayId`, `titleSnapshot`, `statusAtRemoval`                                                                                                                           |
| `run.case_version_applied`        | `caseDisplayId`, `titleSnapshot` (el título nuevo), `fromRevision`, `toRevision`                                                                                              |
| `run.cases_bulk_marked`           | `bulkCases` (por caso `fromStatus` / `toStatus`; `summary.statusFrom` / `statusTo` homogéneos cuando aplica)                                                                  |
| `run.cases_bulk_assigned`         | `bulkCases` (por caso `fromAssignee` / `toAssignee`; resumen homogéneo cuando aplica)                                                                                         |
| `run.cases_bulk_retried`          | `bulkCases` (transiciones por caso; `summary.unchanged` para casos ya sin probar)                                                                                             |
| `run.cases_bulk_removed`          | `bulkCases` (por caso `statusAtRemoval`)                                                                                                                                      |
| `run.cases_bulk_version_applied`  | `bulkCases` (`fromRevision` / `toRevision` por caso; `summary.affected` actualizados, `summary.unchanged` casos desactualizados omitidos)                                     |
| `run.case_step_actual_result_set` | `caseDisplayId`, `titleSnapshot`, `runCaseUlid`, `stepUlid`, `stepPosition`, `stepActionSnapshot`, `changes` (`actual_result` de → a)                                         |
| `run.result_attachment_committed` | `caseDisplayId`, `titleSnapshot`, `runCaseUlid`, `stepUlid`, `stepPosition`, `attachments`; `changes` opcional cuando el texto de `actual_result` cambió en la misma petición |
| `run.result_attachment_deleted`   | Misma forma que `run.result_attachment_committed`, sin `runCaseUlid`                                                                                                          |

`run.reopened` y `run.reran_failed` siguen siendo valores válidos del enum `action` para que las filas históricas sigan analizándose, pero ya no se emiten — Reopen se eliminó y `rerun-failed` se generalizó en `run.cloned` (`POST .../runs/{runUlid}/clone`).

Ejemplo — cierre con scorecard:

```json
{
  "action": "run.closed",
  "entityType": "test_run",
  "entityLabel": "Sprint 24",
  "metadata": {
    "runAggregates": {
      "countPassed": 7,
      "countFailed": 2,
      "countBlocked": 1,
      "countSkipped": 0,
      "countUntested": 0,
      "totalDurationMs": 935000,
      "projectedStatus": "failed"
    }
  }
}
```

Ejemplo — reasignación de caso:

```json
{
  "action": "run.case_assigned",
  "entityType": "test_run_case",
  "entityLabel": "ACME-1 · Flujo de login",
  "metadata": {
    "caseDisplayId": "ACME-1",
    "titleSnapshot": "Flujo de login",
    "assigneeChange": {
      "from": { "ulid": "01J…", "name": "María López" },
      "to": { "ulid": "01J…", "name": "Pedro García" },
      "via": "patch"
    }
  }
}
```

## Mutaciones de ciclo de vida del run (abortar, clonar y eliminar)

Estos endpoints mutan runs y emiten las acciones de auditoría documentadas arriba. **No** forman parte de las rutas de listado de auditoría.

### `POST /api/v1/runs/{runUlid}/abort`

- **Rol**: miembro, admin u owner (`viewer` → `403`)
- **Cuerpo**: ninguno
- **Respuesta**: `200` con el `RunResponse` actualizado (`state: "closed"`, `abortedAt` fijado al momento del abandono)
- **Errores**: `409` si el run no está actualmente abierto (ya cerrado o ya abortado)
- **Auditoría**: `run.aborted` con `metadata.runAggregates` (`projectedStatus` siempre `'aborted'`)
- Los casos sin probar quedan sin probar — abortar no registra resultados de caso. Esto es terminal: un run abortado permanece congelado (las mutaciones de caso devuelven `409`); no existe reapertura. Usa `.../clone` (abajo) para iniciar un run nuevo a partir de él.

### `POST /api/v1/projects/{projectId}/runs/{runUlid}/clone`

"Volver a ejecutar": generaliza el antiguo endpoint `rerun-failed` (eliminado).

- **Rol**: miembro, admin u owner (`viewer` → `403`)
- **Cuerpo**: `{ "title": "string, obligatorio, máx 200", "cloneAssignees"?: boolean, "statusFilter"?: TestOutcomeStatus[] }` — `statusFilter` selecciona qué casos del origen se copian; omitido o vacío significa todos.
- **Respuesta**: `201` con el nuevo `RunResponse` (`state: "open"`)
- **Errores**: `422` si `title` está vacío o `statusFilter` contiene un estado desconocido
- **Auditoría**: `run.cloned` con `metadata.caseCount`, `metadata.sourceRun`, `metadata.cloneAssignees`, `metadata.statusFilter`
- Funciona sobre cualquier run origen cerrado — completado o abortado. Cuando `cloneAssignees` es `true`, cada caso copiado conserva el asignado del caso origen; si no, todos los casos del nuevo run empiezan sin asignar.

### `DELETE /api/v1/runs/{runUlid}`

- **Rol**: solo admin u owner (`member` / `viewer` → `403`)
- **Respuesta**: `204 No Content`
- **Auditoría**: `run.deleted` con `metadata.runAggregates`, `wasState` y `closedAt` capturados antes de la cascada. Tras el borrado, `run_id` en filas de auditoría (incluida esta) pasa a `null` por cascada FK; el evento sigue visible en listados de toda la organización.

## Mutaciones

Los endpoints de **listado** de auditoría son de solo lectura. Usa las rutas de run anteriores (y el resto de APIs del producto) para generar nuevas filas de auditoría.

## Acciones de comentario

Estas acciones se emiten para los comentarios en hilos de las páginas de entidades, incluyendo respuestas (`parentUlid` presente) y fijado/desfijado. Las acciones `commented`, `comment_updated` y `comment_deleted` incluyen `metadata.commentUlid` y `metadata.excerpt` (primeros 240 caracteres del cuerpo del comentario); `comment_pinned` y `comment_unpinned` incluyen solo `metadata.commentUlid` (fijar no lleva extracto del cuerpo).

| Acción                       | Se emite cuando                                                                               | Tipo de entidad |
| ---------------------------- | --------------------------------------------------------------------------------------------- | --------------- |
| `defect.commented`           | Se agrega un comentario o respuesta a un defecto                                              | `defect`        |
| `defect.comment_updated`     | Se edita un comentario en un defecto                                                          | `defect`        |
| `defect.comment_deleted`     | Se elimina un comentario en un defecto (una vez por cada respuesta en cascada también)        | `defect`        |
| `defect.comment_pinned`      | Se fija un comentario de primer nivel en un defecto                                           | `defect`        |
| `defect.comment_unpinned`    | Se desfija un comentario de primer nivel en un defecto                                        | `defect`        |
| `test_case.commented`        | Se agrega un comentario o respuesta a un caso de prueba                                       | `test_case`     |
| `test_case.comment_updated`  | Se edita un comentario en un caso de prueba                                                   | `test_case`     |
| `test_case.comment_deleted`  | Se elimina un comentario en un caso de prueba (una vez por cada respuesta en cascada también) | `test_case`     |
| `test_case.comment_pinned`   | Se fija un comentario de primer nivel en un caso de prueba                                    | `test_case`     |
| `test_case.comment_unpinned` | Se desfija un comentario de primer nivel en un caso de prueba                                 | `test_case`     |

Ejemplo:

```json
{
  "action": "test_case.commented",
  "entityType": "test_case",
  "entityLabel": "TC-1 Login con credenciales válidas",
  "metadata": {
    "commentUlid": "01J…",
    "excerpt": "Reproducido en staging con sesión nueva."
  }
}
```

## Gestión de tokens

Crea y revoca tokens de API en [Autenticación API](https://docs.probara.net/es/developers/authentication/) (en la app: `/user/api-tokens`).
