Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesWorkout ParseWorkout Parse — QA Plan

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_KEY set (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 Run

Scenarios

  1. Freeform first. /dashboard/workouts/new opens freeform directly.
  2. Translation is available. Clicking “Convert to structured workout” replaces the freeform form with the parse input on the same route.
  3. Tier gate. On a Lite org, ?mode=translate shows the upgrade card, never the textarea. Direct API POST /parse → 403 (tier).
  4. 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.
  5. 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 show unresolved chips. 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’s language holds the detected tag.
  6. 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.
  7. Adjustment — pick a candidate. Paste text with an ambiguous movement (e.g. “DB press”). An amber suggested chip 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.
  8. Adjustment — create org-local exercise. Include a made-up movement (e.g. “Bosu burpee slam”). Red unresolved chip → 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.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. “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.
  14. 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.
  15. 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_jobs row is created (blocked before the LLM call).
  16. Member role. Signed in as a member: no entry points; direct API calls to all four parse routes → 403.
  17. 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.
  18. Observability. In PostHog (production/staging with a real key): a parse run emits parse.requested → parse.completed; a commit emits parse.committed with zeroEdit/editDistance; blocked attempts emit parse.blocked with the right reason. No event property contains pasted text, mention text, or titles — counts/enums only. Events carry $groups.organization and appear under an organization-group filter.
  19. 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 auto movement from the auto-matched list and override it.
  20. 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.
  21. 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.
  22. 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_failed inline error; the job row (dev DB) ends failed with error_code.
  • Double-click commit → single workout created; a second attempt (or a stale tab) gets 409 parse_job_closed and 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 the taikan_test DB on 55432).

Performance expectations

  • Parse round-trip (paste → preview) under ~20s for a typical WOD; stage_timings on the job row breaks down extraction vs resolution.
  • Preview interactions (chip popovers, drag, prescription edits) are the builder’s own — no added latency budget.