# Per-track playback flow — on-device test checklist

The parsing/serialization and the `_end_plan`/`_goto_target` decision logic are covered by
`tests/run.mjs` and unit tests. What still needs a real device (PM_K-1 / PM_X-1) is the
**runtime**: that the gapless seam fires stop / advance / relative-goto at the right bar.

## Setup

1. Flash the new firmware: copy `dist/app.mpy` (PM_K) or `dist/explorer-app.mpy` (PM_X) onto the
   device as `/app.mpy` (or use the editor's one-click push — it serves this build).
2. Author the test tracks in the web editor: paste each program string into the program-string
   field, **Save** it as a set-list item, then **Save to device**. (Multi-item cases: create the
   items in order in one set-list.)
3. On the device, select the set-list and press play (Button A).

`b<n>` sets the cycle length in bars; a cycle = `b<bars>`, else one master bar. The action fires
after `rep × cycle` bars (rep defaults to 1).

## Cases

| # | Track(s) | Expect | ✓ |
|---|----------|--------|---|
| 1 | `t120;kick:4` | **Loops forever.** Joystick-right advances manually. | ☐ |
| 2 | `t120;b2;kick:4;end=stop` | Plays **2 bars, then stops** by itself (LED → green, transport stops). | ☐ |
| 3 | `t120;b2;kick:4;rep=3;end=stop` | Plays **6 bars (3×2), then stops.** | ☐ |
| 4 | A=`t120;b2;kick:4;end=next`  B=`t100;b2;snare:4` | A plays 2 bars then **gaplessly advances to B** (no gap, no count-in, tempo jumps to 100). | ☐ |
| 5 | A=`t120;b2;kick:4;rep=2;end=next`  B=`t100;b2;snare:4` | A plays **4 bars** (2×2) then advances to B. | ☐ |
| 6 | 1=`t120;b2;kick:4` 2=`t120;b2;snare:4;end=next` 3=`t120;b2;clap:4;end=-2` | 3 jumps **back 2 → track 1** after its cycle (a looping verse→chorus section). | ☐ |
| 7 | 1=`t120;b2;kick:4;end=-2` (first item) | `end=-2` before the start **clamps to track 1** → it just loops itself. | ☐ |
| 8 | last item `…;end=next` | Advancing past the last item **wraps to the first** (set-list loops). | ☐ |
| 9 | any of the above, mid-flow | **Manual joystick-right / footswitch always advances immediately,** regardless of `end=`. | ☐ |
| 10 | global **Continue ON** + an item `t120;b2;kick:4;end=stop` | The item **still stops** — explicit `end=` overrides the global Continue default. | ☐ |
| 11 | global **Continue ON** + `t120;b2;kick:4` (no `end=`) | **Advances** after 2 bars — legacy behavior preserved (Continue = default `end=next`). | ☐ |

## What to watch for

- **Seam quality** (cases 4–6): the swap should be click-on-the-beat with no audible gap, dropout,
  or double-trigger at the boundary. This is the highest-risk part of the change.
- **Stop cleanliness** (cases 2–3): no extra click after the stop; speaker goes quiet; the play
  session is logged.
- **Ramp/trainer interaction**: a track with `rmp…`/`tr…` *and* an `end=` should ramp/gap normally
  and still fire the end-action at `rep × cycle`.
- **Memory**: relative-goto and advance both go through `_prepare_next` (which `gc.collect()`s
  before parsing). Watch for any MemoryError fallback (it leaves the track looping instead).

Report any case that misbehaves with its number and what happened.
