Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesDaily ProgrammingDaily programming — QA plan

Daily programming — QA plan

Pre-requisites

  • Pro-tier org with:
    • Schedule-mode program “Open Box” containing two class types: “WOD” and “Open Gym”.
    • Library workouts: “Fran”, “Easy Row”, “Skills Day”.
  • Personas: Coach Casey, Owner Olga, Member Mia.
  • Mia has a booking on the WOD session 2026-06-03.
  • Org timezone Asia/Jerusalem.

Test scenarios

Set / replace

  1. Set a slot with one workout

    • Trigger: Casey PUT /daily-programming { classTypeId:<wod>, date:'2026-06-03', workouts:[{workoutId:<fran>, sortOrder:0}] }.
    • Expected: 1 row in daily_programming with status='draft', created_by=Casey. Response is the grouped slot with that one workout.
  2. Set two workouts on the same slot

    • Trigger: same PUT with two workouts at sortOrder 0 and 1.
    • Expected: 2 rows. UNIQUE constraint not violated (different workout_id).
  3. Replace existing slot

    • Trigger: after step 2, PUT again with one different workout.
    • Expected: the previous 2 rows are physically deleted; only the new 1 row remains.
  4. Set with empty workouts[]

    • Trigger: PUT { ..., workouts: [] }.
    • Expected: existing rows deleted; slot effectively empty.
  5. Set with cross-org workoutId

    • Trigger: workoutId from a different org.
    • Expected: 400 “One or more workouts not found in this organization”.
  6. Set with soft-deleted workoutId

    • Trigger: workoutId of a workout with deleted_at set.
    • Expected: 400.
  7. Set with cross-org classTypeId

    • Trigger: classTypeId from another org.
    • Expected: 400 “Class type not found in this organization”.
  8. Duplicate workoutId in the same payload

    • Trigger: workouts:[{wid:X, sortOrder:0}, {wid:X, sortOrder:1}].
    • Expected: UNIQUE (class_type_id, date, workout_id) rejects the second insert. TODO: verify whether service deduplicates first or the DB raises.

Status

  1. Publish a slot

    • Trigger: PATCH /daily-programming/status { classTypeId:<wod>, date:'2026-06-03', status:'published' }.
    • Expected: all rows for the slot become published.
  2. Unpublish back to draft

    • Trigger: same with status:'draft'.
    • Expected: all rows back to draft.
  3. Status flip on empty slot

    • Trigger: PATCH /status on a slot with no rows.
    • Expected: 200, no rows updated, response shows empty slot.

Reads

  1. Week read as coach

    • Trigger: GET /daily-programming?weekStart=2026-06-01.
    • Expected: rows for the whole week, including drafts. Grouped per (classType, date).
  2. Week read as member with drafts present

    • Trigger: same as 12 but as Mia.
    • Expected: drafts filtered out; only published rows visible.
  3. Date read with explicit classType filter

    • Trigger: GET /daily-programming?date=2026-06-03&classTypeId=<wod> (passed via service findByDate with classTypeId).
    • Note: the HTTP GET / does NOT accept classTypeId; the route uses findByDate(orgId, clerkId, date). The classTypeId-scoped read is internal. TODO: verify which is preferred.
  4. Week read returns no rows for empty week

    • Trigger: a week with no programming.
    • Expected: [].
  5. Member sees workout sections

    • Trigger: Mia GET /daily-programming?date=2026-06-03 for a slot that’s published.
    • Expected: response carries the workout with sections + movements + exercise metadata.

Remove

  1. Remove a slot

    • Trigger: DELETE /daily-programming?classTypeId=<wod>&date=2026-06-03.
    • Expected: rows physically deleted. Subsequent GET returns the slot absent (no draft tombstone).
  2. Remove a non-existent slot

    • Trigger: same on an empty slot.
    • Expected: 200, no-op.

Build the week with Spotter (class_sessions.copy_week and friends)

  1. Spotter lists what’s missing, then waits — never authors a workout

    • Trigger: Casey clicks “Build the week with Spotter” on a board with 3 class days that have a session but no WOD (Mon/Wed/Fri).
    • Expected: Spotter’s first reply lists the three gaps from the status message it already received — no tool call yet, no invented workout text, no read.*/class_types.*/daily_programming.list_for_week lookup. It then waits for instructions.
  2. Coach pastes a workout and names the day

    • Trigger: after the gap list, Casey pastes a workout and says “post that to Wednesday’s WOD class.”
    • Expected: workouts.create (freeform, saved exactly as pasted — no rewriting) then daily_programming.set for that (classTypeId, date). One confirmation card.
  3. Coach asks to copy last week

    • Trigger: “copy last week’s schedule into this week.”
    • Expected: class_sessions.copy_week({ programId, fromWeekStart, toWeekStart }) — confirmation card always shown. New sessions land as DRAFT; a target day that already has a session is skipped, never overwritten.
  4. Coach asks to apply a saved timetable

    • Trigger: “apply the Standard Week template” (named in the prompt’s “Timetable templates” line).
    • Expected: program_templates.apply with the named template’s id — the model doesn’t call program_templates.list first since the id is already in the opening message.
  5. Member tries to trigger a schedule-changing turn

    • Trigger: Mia’s session somehow reaches class_sessions.copy_week (shouldn’t be possible from the member app, but exercise the guard directly).
    • Expected: 403 — same owner/admin/coach gate as the rest of class_sessions.

Spotter prompt contract (client-built, buildSpotterWeekPrompt)

  1. Button sends a fully-specified status message, no discovery calls

    • Trigger: click “Build the week with Spotter” on a board with a mix of no_session/empty/draft/published cells across 2 class types, one session with no coach assigned.
    • Expected: the first user message already lists, per class type, session days / WOD-posted days / no-WOD-yet days (or “no sessions this week”), the coach-gap and draft counts, what ran last week but not this week (if the fetch succeeded), and any saved timetable templates — Spotter never needs a read.*/class_types.*/daily_programming.list_for_week call to answer “what’s missing.”
  2. Last week’s board fetch, or the templates fetch, fails

    • Trigger: simulate a 500/timeout on the last-week GET /daily-programming/board fetch, then separately on the GET /program-templates fetch.
    • Expected: the prompt still sends in both cases — only the affected line (“Ran last week but not this week” / “Timetable templates”) is omitted; nothing blocks the turn.
  3. Prompt in Hebrew and Russian

    • Trigger: Casey’s dashboard locale is he, then ru; click Build the week with Spotter in each.
    • Expected: the message body (headers, the ask, the rules) is in the coach’s locale; dates and weekday abbreviations are locale-formatted (e.g. Hebrew weekday names, Russian month abbreviations); {classTypeId}/ISO dates inside a line stay unlocalized (the model needs exact values).

Template apply integration

  1. Apply schedule template writes daily-programming
    • Trigger: POST /program-templates/:id/apply with mode schedule, startDate.
    • Expected: new daily_programming rows (status='draft') for each cell that lands on an active day. Coach must publish manually.

Permissions

  1. Member tries to PUT

    • Trigger: Mia PUT /daily-programming.
    • Expected: 403.
  2. Member tries to PATCH status

    • Trigger: Mia PATCH /daily-programming/status.
    • Expected: 403.
  3. Cross-org read

    • Trigger: Coach Bob (org B) calls org A’s endpoint.
    • Expected: 403 from requireMembership.

Class-session integration

  1. Class session card shows the day’s published workouts

    • Trigger: Mia opens the WOD session for 2026-06-03 on her schedule.
    • Expected: the session detail loads via ClassSessionsService.findByDateRange and renders Fran. If the slot is draft, Mia sees no workout.
  2. Class session card shows nothing in draft

    • Trigger: same after unpublishing.
    • Expected: workout disappears from member’s view.

i18n

  1. Whiteboard in Hebrew

    • Trigger: Mia in he views the WOD board.
    • Expected: section labels, movement names (canonical with Hebrew override), and time-cap labels all RTL/translated.
  2. Coach UI in Russian

    • Trigger: Casey in ru sets the day’s programming.
    • Expected: status chips (“Draft”/“Published”) and form labels translated.

What “broken” looks like

  • A previously-removed workout reappears after a status flip (because PATCH /status doesn’t restore physical deletes — but if a bug re-inserted rows on flip, would show this symptom).
  • Member sees draft rows (member visibility filter regressed).
  • Two coaches editing the same slot simultaneously each end up with their own row set (race in the delete-then-insert transaction).
  • Cross-org workout slips into the slot (workoutId org validation missing).
  • Daily programming for a deleted class type persists and renders ghost entries (the class type FK is set NULL’d? — verify CASCADE behavior; current schema does not specify ON DELETE on class_type_id).
  • Member sees programming for a class type that doesn’t belong to their org (org scoping via getOrgClassTypeIds regressed).
  • Status enum drift — service accepts arbitrary strings instead of draft/published (currently the IsIn validator on the DTO enforces this).
  • Spotter authors a workout on its own instead of listing gaps and waiting (a regression of the no-authoring product stance, not a service-level bug — watch for it in transcript review).
  • class_sessions.copy_week overwrites a target day that already has a session, or lands sessions as anything but DRAFT (both break the “never overwrites, always draft” contract copy-week shares with program_templates.apply’s skip mode).