# 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 - [x] 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. - [x] Path traversal validation still happens in exactly one place (`disk_path`) and applies to every instance. - [x] `TrackFile::from_track` is the single Track→file conversion site, and it always emits a `link` playable (no special cases per provider). - [x] Serialization never panics: `to_toml` returns `TrackFileError::Serialize`, and every `Option` field is skip-serialized (TOML cannot represent `None`). - [x] 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 - [x] `QueueStore` is the only writer of the queues directory; the `/queues` provider instance only reads. - [x] 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. - [x] `save` validates the name first and never touches disk for an invalid name or empty snapshot. - [x] `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. - [x] File names come from `fsdy::track_file_name` — no second naming scheme. - [x] No file contents in logs anywhere in the store (paths and names only). ## Playback wiring - [x] 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. - [x] Every queue-state change reaches the persist channel: queue content changes (the `broadcast_queue` funnel), current-track changes (`play`), and the shuffle/repeat toggles. - [x] The persister task debounces and skips snapshots equal to the last one written (pure `resolving`-flag broadcasts must not rewrite the folder). - [x] Persist failures are warnings; no persist error can stop playback or crash the loop. - [x] The startup restore runs before the playback loop serves commands, restores tracks + position + repeat/shuffle, and never starts playback (`PlayState::Stopped`). - [x] Restore tolerates a corrupt position (out of range → clamped or reset, never a panic). - [x] A server without a usable queues directory (no config dir, mkdir fails) runs without persistence after a warning — never dies. ## RPC and orchestrator - [x] `save_queue` maps errors: invalid name → `invalid_argument`, empty queue → `failed_precondition`, I/O → `internal`; no `color-eyre`/debug reports leak to clients. - [x] 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. - [x] `/queues` mutations via the library stay `NotSupported` (create/rename/delete unchanged). ## TUI - [x] `w` is bound in `Scope::Queue` only, has a help description, and passes the existing bindings-table invariant tests unchanged. - [x] The save overlay reuses `InputState` (Esc cancels, Enter submits trimmed, empty submit closes silently) and is a no-op while the queue is empty. - [x] `MessageFromUi::SaveQueue` reaches the `SaveQueue` RPC; a failed save must not crash the TUI. ## Hygiene - [x] New public items are documented; docs state error/edge behavior. - [x] `clippy -D warnings`, `fmt`, `taplo`, `markdownlint` clean on the whole workspace; all tests green. - [x] `architecture/fs-provider.md` reconciled: the "no links into /fs" rule replaced by the one-hop semantics, D2's "chains structurally impossible" wording updated.