```yaml
id: UJ-020
title: Cancel pending translation job
actors: [administrator]
surface: admin-spa
goal: Administrator cancels one pending job and verifies persisted cancellation state.
trigger: Activate Cancel on pending row at /wp-admin/admin.php?page=international-press-zone#/history.
fixtures: [authenticated-admin, pending-translation-job]
success_state:
  visible: Job #N cancelled
  durable: Selected job is failed with Cancelled by user error.
  persistence: Fresh job list returns cancellation state.
source_specs: [admin/src/pages/translate-history.js, includes/API/TranslateJobsController.php]
readiness: draft
canonical: false
blockers: []
```

## Path

| Hop | Evidence |
|---|---|
| UI trigger | `admin/src/pages/translate-history.js:404` |
| Request construction | `admin/src/pages/translate-history.js:759` |
| Endpoint auth and parse | `includes/API/TranslateJobsController.php:148` |
| Authoritative write | `includes/API/TranslateJobsController.php:565` |
| Response | `includes/API/TranslateJobsController.php:577` |
| Terminal render | `admin/src/pages/translate-history.js:760` |

## Happy path

### H1
- Setup: Administrator authenticated; one named pending job exists.
- Action: Activate `Cancel` on named job.
- Request: `POST /wp-json/international-press-zone/v1/translate-jobs/{id}/cancel`.
- Response: `200` with `{success:true,message:"Translation job cancelled successfully"}`.
- Visible: `Job #N cancelled` renders and refreshed row is failed.
- Durable: Named job has `status:"failed"` and `error_message:"Cancelled by user"`.
- Fresh read: `GET /wp-json/international-press-zone/v1/translate-jobs` returns named job with cancellation state.
- Forbidden: Other jobs, translations, content, strings, settings, and licenses remain unchanged.
- Evidence:
  - Setup: `includes/API/TranslateJobsController.php:548`
  - Action: `admin/src/pages/translate-history.js:404`
  - Request: `admin/src/pages/translate-history.js:759`
  - Response: `includes/API/TranslateJobsController.php:577`
  - Visible: `admin/src/pages/translate-history.js:760`
  - Durable: `includes/API/TranslateJobsController.php:565`
  - Fresh read: `includes/API/TranslateJobsController.php:78`
  - Forbidden: `includes/API/TranslateJobsController.php:565`

## Alternate and failure paths

### A1
- Setup: Administrator authenticated; selected job is completed.
- Action: Submit cancel for completed job.
- Request: `POST /wp-json/international-press-zone/v1/translate-jobs/{id}/cancel`.
- Response: `400` with `{code:"invalid_status",message:"Only pending or processing jobs can be cancelled.",data:{status:400}}`.
- Visible: `Only pending or processing jobs can be cancelled.` renders.
- Durable: Completed job remains unchanged.
- Fresh read: Job-list GET returns original completed state.
- Forbidden: No job status, error, translation, or content changes.
- Evidence:
  - Setup: `includes/API/TranslateJobsController.php:560`
  - Action: `admin/src/pages/translate-history.js:757`
  - Request: `admin/src/pages/translate-history.js:759`
  - Response: `includes/API/TranslateJobsController.php:561`
  - Visible: `admin/src/pages/translate-history.js:764`
  - Durable: `includes/API/TranslateJobsController.php:560`
  - Fresh read: `includes/API/TranslateJobsController.php:78`
  - Forbidden: `includes/API/TranslateJobsController.php:560`

## Permissions and boundaries

### P1
- Setup: Actor lacks translation-management capability; pending job exists.
- Action: Submit cancel request.
- Request: `POST /wp-json/international-press-zone/v1/translate-jobs/{id}/cancel`.
- Response: `401` with `{code:"rest_forbidden",message:"Sorry, you are not allowed to do that.",data:{status:401}}`.
- Visible: REST denial exposes no job detail.
- Durable: Pending job remains unchanged.
- Fresh read: Protected job-list request returns permission denial.
- Forbidden: No status, error, translation, or content change occurs.
- Evidence:
  - Setup: `includes/API/TranslateJobsController.php:615`
  - Action: `includes/API/TranslateJobsController.php:153`
  - Request: `includes/API/TranslateJobsController.php:148`
  - Response: `includes/API/TranslateJobsController.php:615`
  - Visible: `includes/API/TranslateJobsController.php:615`
  - Durable: `includes/API/TranslateJobsController.php:615`
  - Fresh read: `includes/API/TranslateJobsController.php:78`
  - Forbidden: `includes/API/TranslateJobsController.php:615`

## Source specs

- `admin/src/pages/translate-history.js`
- `includes/API/TranslateJobsController.php`
