97 lines
5.0 KiB
Markdown
97 lines
5.0 KiB
Markdown
# Quality gates — seek
|
||
|
||
Criteria an implementation of `architecture/seek.md` must satisfy.
|
||
Automated coverage lives in `audio-player/src/player_engine.rs` (the
|
||
clamping arithmetic, which is where the interesting behaviour is),
|
||
`crabidy-server/src/playback.rs`, and the two clients' binding tests.
|
||
|
||
## Hard rules (highest priority)
|
||
|
||
- [ ] **G1 — No panic on any delta, from any state.** The clamp is
|
||
saturating arithmetic with no `assert`/`clamp(min, max)` where
|
||
`min > max` is reachable. Specifically: an unknown duration (reported
|
||
as 0), a track shorter than the step, a delta larger than the track,
|
||
`i64::MIN`/`i64::MAX` as the delta, and a stopped or empty sink must
|
||
all be handled without panicking. This closes the existing bug in
|
||
`seek_to` (D5). *(tests: `seek_target_never_panics_on_extremes`,
|
||
`seek_target_saturates_at_the_start`,
|
||
`an_unknown_duration_does_not_clamp_forward`.)*
|
||
- [ ] **G2 — A failed seek stays server-side.** `Seek` never returns a
|
||
`color-eyre`/`anyhow` report to a client; an unseekable source produces
|
||
a `warn!` and an unchanged position, not an RPC error (D7).
|
||
- [ ] **G3 — No new unbounded channel and no new blocking call on an
|
||
async task.** The seek reply rides the existing bounded reply pattern;
|
||
`try_seek` is called only from the engine thread.
|
||
|
||
## Behaviour
|
||
|
||
- [ ] **G4 — Deltas compose.** Two successive backward seeks from the
|
||
same starting position land at `start - 2 × step`, not
|
||
`start - step` — the engine reads the live position each time (D1).
|
||
*(test: `successive_seeks_compose`.)*
|
||
- [ ] **G5 — Backward saturates at zero**, never below, and never into
|
||
the previous track (A1).
|
||
- [ ] **G6 — Forward past the end lands one second before the end**
|
||
when the duration is known, so the track finishes through the normal
|
||
end-of-stream path and the queue advances — the engine never seeks to
|
||
the exact end (D4). *(test: `seek_target_saturates_near_the_end`.)*
|
||
- [ ] **G7 — An unknown duration does not clamp forward.** A
|
||
length-less stream reports duration 0; that must not turn every
|
||
forward seek into "seek to 0" (the naive clamp) — it must pass the
|
||
target through and let the decoder refuse it.
|
||
- [ ] **G8 — The new position is broadcast immediately**, including
|
||
while **paused** — `tick()` returns early for a paused sink, so the
|
||
engine emits `Elapsed` from the seek path itself (D6).
|
||
- [ ] **G9 — Seeking while stopped does nothing** and does not start
|
||
playback (A4).
|
||
|
||
## Wire and plumbing
|
||
|
||
- [ ] **G10 — One RPC, relative only**: `Seek(SeekRequest{sint32
|
||
delta_millis}) -> SeekResponse`, fire-and-forget like the other
|
||
playback RPCs (D2, A3). No absolute field, no oneof — the deferred
|
||
extension stays compatible.
|
||
- [ ] **G11 — The delta is never re-derived client-side.** No client
|
||
computes a target position from a broadcast `TrackPosition`; grep for
|
||
arithmetic on `position` in the clients and find none (D1, A2).
|
||
*(test: `seek_command_forwards_the_delta_unchanged`.)*
|
||
- [ ] **G12 — The playback loop holds no seek state.** `PlaybackCommand::Seek`
|
||
forwards to the player and does nothing else — no position cache to go
|
||
stale.
|
||
|
||
## Clients
|
||
|
||
- [ ] **G13 — `,`/`.` seek and `<`/`>` skip tracks, global in both
|
||
clients**, colliding with nothing in any scope, with the TUI's
|
||
binding-table uniqueness invariant intact and the shifted forms
|
||
resolving whether or not the terminal reports `SHIFT`. Being plain
|
||
printable characters, none of them can be swallowed by a browser — which
|
||
`Ctrl-n` is, and which is why `<`/`>` exist. *(tests: the
|
||
`bindings`/`keymap` tables and their uniqueness tests, extended.)*
|
||
- [ ] **G18 — A progress-bar click becomes an offset inside the track.**
|
||
The mapping is a pure function tested on the native target: a click
|
||
behind the playhead seeks back, the edges are exactly the track's ends, a
|
||
fraction outside `[0, 1]` is clamped rather than extrapolated, and a
|
||
duration of 0 (or a non-finite fraction, meaning a zero-width element)
|
||
declines instead of seeking somewhere arbitrary. *(tests:
|
||
`a_progress_bar_click_becomes_an_offset`,
|
||
`a_click_without_a_scale_is_declined`.)*
|
||
- [ ] **G14 — Every client reaches it**: TUI binding → `MessageFromUi`
|
||
→ RPC; web binding + `⏪`/`⏩` buttons → RPC; `cbd global seek
|
||
<SECONDS>` accepting a negative value (D8).
|
||
- [ ] **G15 — Both help surfaces list it** — the TUI help modal (from
|
||
`BINDINGS`) and the web help overlay (`HELP`) — so the two tables stay
|
||
in lockstep with dispatch, which their tests enforce.
|
||
|
||
## Documentation
|
||
|
||
- [ ] **G16 — Documented where a user looks**: the key tables in
|
||
`docs/src/clients/tui.md` and the web page, the CLI page's `global`
|
||
list, and a sentence in `docs/src/queue.md` saying seek is
|
||
within-track, that a forward seek past the end advances, and that
|
||
SoundCloud's HLS streams cannot seek.
|
||
- [ ] **G17 — Clippy clean under `-D warnings`** across the feature
|
||
matrix (`check-features`), fmt clean, and the wasm target builds — the
|
||
web client's `mod app` is `#[cfg(target_arch = "wasm32")]` and native
|
||
clippy does not see it.
|