Workout Parse — QA Plan
Manual test script for the paste-to-structure flow. Automated coverage exists (apps/web/e2e/specs/paste-workout.spec.ts + unit/integration specs); this plan is the human pass over the pieces automation can’t judge — real LLM output quality, copy, RTL, and the cross-org/tier gates.
Setup
- Org on tier Pro (has
workout_builder), owner role. - Canonical exercise library seeded (
pnpm seed:movements). ANTHROPIC_API_KEYset (scenarios 4+ make real Sonnet calls; an unchanged re-paste in the same org is served from the extraction cache).- A second org on Lite tier for scenario 3.
Sample paste (used in several scenarios):
Strength
Back Squat 5x5 @ 80%
Metcon — AMRAP 12
10 Thrusters 42.5/30
15 Pull-ups
200m RunScenarios
- Freeform first.
/dashboard/workouts/newopens freeform directly. - Translation is available. Clicking “Convert to structured workout” replaces the freeform form with the parse input on the same route.
- Tier gate. On a Lite org,
?mode=translateshows the upgrade card, never the textarea. Direct APIPOST /parse→ 403 (tier). - Golden path — paste, preview, commit. Paste the sample text and submit. Expect the original source with movement highlights alongside two structured sections. Commit → success toast → structured workout opens in the normal builder. Its description contains only explicit coach comments, not the pasted source; the parse job retains the complete source. For the Service Cup fixture, verify two sections/nine movements, Part A cap 15 minutes and shuttle definition, female 95/male 135 lb on its four lifts only, and Part B start 900/duration 180 seconds.
- Non-English paste. Paste a Hebrew workout (e.g.
אימון כוח: סקוואט 5x5). Expect a normal preview — no language error, source retained literally; movements the catalog can’t match showunresolvedchips. Then paste English programming under a Hebrew coaching preamble: the preamble becomes the coach note and the sections carry the English programming. The job row’slanguageholds the detected tag. - Multi-workout warning (D4). Paste a week of workouts separated by day headers (
Day 1 …\nDay 2 …). Expect the warning banner (parse-multi-warning) and a preview containing only the first day’s content; later days appear nowhere in the tree. - Adjustment — pick a candidate. Paste text with an ambiguous movement (e.g. “DB press”). An amber
suggestedchip appears; clicking opens the popover showing the mention, the raw line, and up to 5 candidates with qualitative confidence labels (no raw numbers). Pick one → chip clears, outstanding counter decrements. - Adjustment — create org-local exercise. Include a made-up movement (e.g. “Bosu burpee slam”). Red
unresolvedchip → popover → “Create new exercise” (name prefilled). Existing library matches for the typed name appear above the create button; choosing one resolves the movement as a library pick. Otherwise create → movement resolves to the new exercise (POST /exercises/upsert; creating the same name again reuses it). Then verify in a second parse of the same text that the new exercise matches by its exact name. - Adjustment — drop to note. On another unresolved movement choose “Drop to note”. The movement row leaves the tree and its raw line appears in the section description.
- Commit gating. While any suggested/unresolved movement is unactioned: commit button disabled, “N movements need attention” counter visible, tooltip on hover explains why. After actioning all → enabled.
- Discard. Click discard → confirmation dialog → confirm. Toast, back to
the root creator’s freeform form. Re-open the job via its
?jobId=URL: preview renders with a “discarded” alert and “Start a new conversion”, and no review controls or commit/discard bar (terminal status). Re-open a committed job’s?jobId=URL: it redirects to the workout. - Refresh / deep link. After a parse, make a few review decisions, then reload the
?jobId=URL: the same draft renders (GET endpoint), including chips, remainder and the decisions just made. Open the same URL as a coach of a different org → 404/not-found behavior, never another org’s draft. - “Convert to structured workout” flow (entry b). Create a freeform workout with body text, save, reopen it, and press “Convert to structured workout”. Parse + commit. Expect the same workout id in structured mode with sections and the old freeform body cleared. An explicitly supplied coach description must survive; the parse job retains the original body.
- Too-long input (D13). Paste > 10,000 chars. The char counter turns destructive and submit is disabled client-side; forcing the API call returns 422
parse_too_long. - Budget breach. With the org at/over its AI budget (or tier caps lowered in a dev DB): submit → inline 429 error with the localized budget message including the period. No
ai_parse_jobsrow is created (blocked before the LLM call). - Member role. Signed in as a member: no entry points; direct API calls to all four parse routes → 403.
- RTL pass (HE locale). Switch the dashboard to Hebrew and rerun scenarios 4–11 visually: page layout mirrors correctly (logical properties); the paste textarea honors
dir="auto"(English paste stays LTR inside an RTL page); chips, popover, commit bar, dialogs, and toasts are correctly aligned and fully translated (workouts.parse.*— no English fallbacks); prev/next-style arrows are NOT double-flipped. - Observability. In PostHog (production/staging with a real key): a parse run emits
parse.requested→parse.completed; a commit emitsparse.committedwithzeroEdit/editDistance; blocked attempts emitparse.blockedwith the rightreason. No event property contains pasted text, mention text, or titles — counts/enums only. Events carry$groups.organizationand appear under an organization-group filter. - Same mention, one decision. Paste a workout where one unmatched movement appears in two sections. Resolve it once: the other occurrence resolves too and an undo toast appears. Reopen an
automovement from the auto-matched list and override it. - Re-convert keeps decisions. Make review decisions, edit the source text and convert again: decisions carry onto movements with the same mention (a toast counts them), and the previous job ends
discarded. - Timing that wasn’t detected. Open a draft (e.g. an older
?jobId=) with a section whose timer config the server would reject. Expect an amber chip naming the section and a commit that saves it as untimed text; a section with any other invalid field disables Publish/Save and the alert names it. - Hebrew structure. Paste
B. Strength – 4 סבביםwith- 3 סטים × 10 חזרותunder a movement and a load inק״ג: the block is 4 rounds (not for time), the line becomes the movement’s sets/reps, and the load stays on the movement.
Negative / robustness
- Submit with an empty textarea → button disabled.
- Kill the API mid-parse → generic
parse_failedinline error; the job row (dev DB) endsfailedwitherror_code. - Double-click commit → single workout created; a second attempt (or a stale tab) gets 409
parse_job_closedand lands on the committed workout. - Force a commit 409 (e.g. a stale review in another tab): the inline alert names the movement or section, offers no “try again”, and the commit buttons stay disabled until the review changes.
- e2e suite:
make test-e2e-web(e2e must target thetaikan_testDB on 55432).
Performance expectations
- Parse round-trip (paste → preview) under ~20s for a typical WOD;
stage_timingson the job row breaks down extraction vs resolution. - Preview interactions (chip popovers, drag, prescription edits) are the builder’s own — no added latency budget.