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. - PostHog flag
workout-parse-transformerenabled for the org (or dev env overrideFEATURE_FLAGS=workout-parse-transformer:trueon the API +NEXT_PUBLIC_FEATURE_FLAGS=workout-parse-transformer:trueon the web). - Canonical exercise library seeded (
pnpm seed:movements). ANTHROPIC_API_KEYset (scenarios 4+ make one real Sonnet call each, ~5–15s).- 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
- Flag off — feature invisible. With the flag disabled: the workout-type chooser (
/dashboard/workouts/new) shows no “Paste & structure” card; the freeform form shows no “Structure this” button; deep-linking to/dashboard/workouts/new/pasterenders the “not available” state (parse-unavailable), not the paste UI;POST /organizations/:orgId/workouts/parsevia API returns 403parse_feature_disabled. - Flag on — entry points appear. Enable the flag. Chooser shows the paste card first; freeform form shows “Structure this”. No other behavior changed anywhere else in workouts.
- Tier gate. On the Lite org (flag on): the chooser card renders dimmed with a lock icon; clicking it lands on the paste route showing the upgrade card (FeatureGate), never the textarea. Direct API
POST /parse→ 403 (tier). - Golden path — paste, preview, commit. Paste the sample text, submit. Expect: spinner ≤ ~20s, then a side-by-side preview — left pane shows the original text with movement spans highlighted; right pane shows 2 sections (a linear/rep-scheme strength section, an AMRAP section with
durationMinutes: 12in its config). Movements matched with green (auto) chips where the library has clear matches. Hovering a movement row highlights its source span and vice-versa. Commit (resolve any flagged movements first, scenario 7) → success toast → redirected to the workout detail page; the workout isstructured, opens in the normal builder, and its description contains the original pasted text. - Non-English rejection (D3). Paste a Hebrew workout (e.g.
אימון כוח: סקוואט 5x5). Expect 422 with the localized “English only” inline error — polite, no partial parse rendered. Verify the message in HE and RU locales too. - 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) → create. Expect success toast, movement resolves to the new exercise. Then verify in a second parse of the same text that the new exercise now auto/suggest-matches (embedding enrichment on create, D10 — allow the queue a minute). - 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 at the chooser. Re-open the job via its
?jobId=URL: preview renders but no commit/discard bar (terminal status). - Refresh / deep link. After a parse, reload the
?jobId=URL: the same draft renders (GET endpoint), including chips and remainder. Open the same URL as a coach of a different org → 404/not-found behavior, never another org’s draft. - “Structure this” flow (entry b). Create a freeform workout with body text, save, reopen it, press “Structure this”. The paste page opens prefilled with the description. Parse + commit. Expect the same workout (same id) flipped to
structuredwith sections — not a new workout — and its description preserved. - 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.organization.
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; second attempt (or a stale tab) surfaces the generic failure toast (409 server-side).
- e2e suite:
make test-e2e-web(the script defaultsFEATURE_FLAGS/NEXT_PUBLIC_FEATURE_FLAGSto enable the parse flag; 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.