This is a 90-minute senior full-stack exercise. You are joining a small working system that simulates asynchronous video-processing jobs. Creating, listing, polling, inspecting, completing, and deterministically failing jobs already work.
Your task is to deliver one production-minded vertical slice: Cancel + Retry. The interesting part is not the two buttons. It is keeping the API, background runner, concurrent requests, and optimistic UI consistent when operations race.
- API: Python 3.11+, FastAPI, Pydantic, pytest
- Web: Node 20+, React, TypeScript, Vite, Vitest
- No database, queue, Docker, or external services are required
Install everything:
make setupConfirm the starter is healthy:
make checkThe supplied acceptance tests describe the unfinished behavior and are intentionally red at the start:
make acceptanceBefore implementation this command should report 9 failing backend acceptance tests and 4 failing frontend acceptance tests. Do not delete, skip, weaken, or rewrite those tests.
Run the app in two terminals:
make run-api
make run-webOpen http://localhost:5173. API docs are at http://localhost:8000/docs.
Add a CANCELLED job status and these endpoints:
POST /jobs/{id}/cancel
POST /jobs/{id}/retry
The state transitions are:
QUEUED ──────┐
├── cancel ──> CANCELLED ──┐
PROCESSING ──┘ │
├── retry ──> QUEUED
FAILED ─────────────────────────────────┘
QUEUEDorPROCESSING→CANCELLED.- Preserve the progress reached when cancellation wins.
- Clear
outputUrland append a timeline event. - Cancelling an already
CANCELLEDjob is idempotent: return200without appending another cancellation event. - Cancelling
DONEorFAILEDreturns409 Conflict.
FAILEDorCANCELLED→QUEUED.- Increment
attempt, reset progress to0, clearerrorandoutputUrl, and append a timeline event. - Start exactly one new background attempt.
- Retrying
QUEUED,PROCESSING, orDONEreturns409 Conflict.
Both endpoints return the updated Job on success. Missing jobs return
404 with {"detail": "Job not found"}. Invalid transitions return 409
with a stable, useful detail string.
Treat these as correctness requirements, not optional hardening:
- Two concurrent retry requests for the same retryable job result in exactly
one
200and one409. - An older attempt must never update a newer attempt after retry.
- Once cancellation wins, the cancelled attempt must never mutate progress, status, error, or output afterward.
Keep transitions atomic in the store. The background simulator already carries an attempt token; preserve and complete that fencing model rather than replacing it with timing assumptions.
Complete the API client and expose actions in the existing job details drawer:
- Show Cancel job only for
QUEUEDandPROCESSING. - Show Retry job only for
FAILEDandCANCELLED. - Track pending state per job/action so rapid duplicate clicks issue only one request and the in-flight action is visibly disabled.
- Reconcile the returned job into both the list and open drawer without a full-list refresh.
- Surface an action error and keep the last valid job state intact.
Do not remove the existing optimistic creation or smart polling behavior.
The acceptance suites are executable requirements:
make backend-acceptance
make frontend-acceptanceAdd at least:
- one meaningful backend test beyond the supplied acceptance tests; and
- one meaningful frontend test beyond the supplied acceptance tests.
Avoid tests that merely assert a mock was called. Prefer a state-machine edge, race, error recovery, or observable user behavior.
The task is complete when:
make verifypasses without modifying the published acceptance criteria.
- 0–10 min: read the model, store, runner, UI, and tests; state your plan.
- 10–70 min: implement the smallest correct vertical slice.
- 70–85 min: add tests and close race/error gaps.
- 85–90 min: run verification and explain production trade-offs.
We value a coherent, verified slice over a large unfinished rewrite. Explain assumptions and trade-offs as you work.
api/app/models.py Job contract and validation
api/app/store.py In-memory atomic mutation boundary
api/app/runner.py Deterministic asynchronous simulator
api/app/main.py FastAPI routes
api/tests/acceptance/ Published backend contract
web/src/api.ts HTTP client
web/src/JobsPage.tsx State, reconciliation, and actions
web/src/components/ Form, list, and details drawer
web/src/hooks/ Visibility-aware polling
web/src/acceptance/ Published frontend contract
The rubric is public; there are no hidden product requirements.
| Area | Points | What strong evidence looks like |
|---|---|---|
| API state machine | 30 | Exact transitions, response codes, resets, events, and idempotency |
| Race safety | 25 | Atomic compare-and-set behavior, one retry winner, stale-attempt fencing |
| Frontend integration | 20 | Correct visibility, duplicate prevention, response reconciliation, errors |
| Tests and verification | 15 | Existing suites preserved, useful new tests, clean lint/build |
| Senior reasoning | 10 | Clear plan, focused changes, explicit trade-offs and production path |
Do not spend the live session adding a database, authentication, a real task queue, SSE/WebSockets, media processing, deployment, or a visual redesign.
If this were production, we would discuss durable compare-and-set updates, idempotency keys, distributed worker cancellation, an outbox/event log, authorization, observability, and push updates. Those are valuable discussion topics; they are not required implementation here.
CI runs the healthy baseline suites, lint, and build. The separate acceptance suites remain red until the task is implemented so the starter repository itself can stay green and reproducible.