crabidy/quality/queue-persistence.md

4.2 KiB

Quality gates: queue-persistence

Criteria the implementation must satisfy beyond the automatic tests (fsdy/src/lib.rs, crabidy-server/src/queue_store.rs, plus the playback and TUI tests added during implementation). Each gate is pass/fail by reading the code.

fsdy instances and serialization

  • Every occurrence of the hardcoded "/fs"/"/fs/" prefix inside fsdy::Client methods now derives from the instance's provider_root; PROVIDER_ROOT remains only as the default instance's constant (init) and for external callers.
  • Path traversal validation still happens in exactly one place (disk_path) and applies to every instance.
  • TrackFile::from_track is the single Track→file conversion site, and it always emits a link playable (no special cases per provider).
  • Serialization never panics: to_toml returns TrackFileError::Serialize, and every Option field is skip-serialized (TOML cannot represent None).
  • The one-hop link argument holds in code: get_urls_for_track still returns MalformedPath for a link playable, so removing the LinkIntoFs rejection cannot introduce recursion anywhere.

Queue store

  • QueueStore is the only writer of the queues directory; the /queues provider instance only reads.
  • Writes are tmp-and-swap: entries are written to a hidden (dot-prefixed) temp sibling, then the old folder is removed and the temp renamed. No code path writes entries into the live folder directly.
  • save validates the name first and never touches disk for an invalid name or empty snapshot.
  • load_current never fails the server: missing folder → None, broken entry → skip with a warning naming the file (never its contents), broken sidecar → default state with a warning.
  • File names come from fsdy::track_file_name — no second naming scheme.
  • No file contents in logs anywhere in the store (paths and names only).

Playback wiring

  • The playback loop never blocks on disk: auto-persist goes through the watch channel (latest wins), SaveQueue writes on a spawned task that reports back through the command's result channel.
  • Every queue-state change reaches the persist channel: queue content changes (the broadcast_queue funnel), current-track changes (play), and the shuffle/repeat toggles.
  • The persister task debounces and skips snapshots equal to the last one written (pure resolving-flag broadcasts must not rewrite the folder).
  • Persist failures are warnings; no persist error can stop playback or crash the loop.
  • The startup restore runs before the playback loop serves commands, restores tracks + position + repeat/shuffle, and never starts playback (PlayState::Stopped).
  • Restore tolerates a corrupt position (out of range → clamped or reset, never a panic).
  • A server without a usable queues directory (no config dir, mkdir fails) runs without persistence after a warning — never dies.

RPC and orchestrator

  • save_queue maps errors: invalid name → invalid_argument, empty queue → failed_precondition, I/O → internal; no color-eyre/debug reports leak to clients.
  • The orchestrator routes /queues in every ProviderClient method (same completeness as /fs), and get_lib_root lists the queues child only when the instance exists.
  • /queues mutations via the library stay NotSupported (create/rename/delete unchanged).

TUI

  • w is bound in Scope::Queue only, has a help description, and passes the existing bindings-table invariant tests unchanged.
  • The save overlay reuses InputState (Esc cancels, Enter submits trimmed, empty submit closes silently) and is a no-op while the queue is empty.
  • MessageFromUi::SaveQueue reaches the SaveQueue RPC; a failed save must not crash the TUI.

Hygiene

  • New public items are documented; docs state error/edge behavior.
  • clippy -D warnings, fmt, taplo, markdownlint clean on the whole workspace; all tests green.
  • architecture/fs-provider.md reconciled: the "no links into /fs" rule replaced by the one-hop semantics, D2's "chains structurally impossible" wording updated.