101 lines
5.5 KiB
Markdown
101 lines
5.5 KiB
Markdown
# Quality gates — RSS provider
|
|
|
|
Criteria an implementation of `architecture/rss-provider.md` must satisfy.
|
|
Automated coverage lives in `rssdy/src/tests.rs` (unit, over a fake
|
|
`Feeds`), plus the settings/toggle tests in `crabidy-server`.
|
|
|
|
## Secrets (hard rule — highest priority)
|
|
|
|
- [ ] **G1 — A feed URL never reaches a log, an error, or a path.**
|
|
`FeedEntry`, `Settings`, `Client`, and `FeedFetcher` `Debug` output
|
|
redact urls (manual impls, not derived). `FetchError` variants carry
|
|
status/parse context only. No `debug!`/`warn!`/`error!` call takes a
|
|
feed or enclosure URL. *(tests: `settings_debug_redacts_urls`,
|
|
`feed_entry_debug_redacts_the_url`.)*
|
|
- [ ] **G2 — No path contains a URL.** Subscription paths are
|
|
`/rss/<slug>`, episode paths `/rss/<slug>/<16 hex>`. A tokened URL
|
|
therefore cannot leak through the UI, the queue, `current`, or a
|
|
bookmark toml. *(tests: `episode_paths_carry_only_slug_and_key`.)*
|
|
|
|
## Freshness — the point of the feature
|
|
|
|
- [ ] **G3 — Listing a subscription always fetches.** Two listings of the
|
|
same node cause two fetches, and the second reflects a feed that gained
|
|
an episode in between. *(test: `every_listing_refetches_the_feed`.)*
|
|
- [ ] **G4 — The memo is written by listings and read only by track
|
|
lookups.** After one listing, resolving every episode of that feed
|
|
costs **no** further fetches; a track lookup with a cold memo fetches
|
|
once. *(tests: `track_lookups_reuse_the_listing_fetch`,
|
|
`a_cold_memo_fetches_once`.)*
|
|
- [ ] **G5 — The memo is bounded** to `MEMO_CAPACITY` feeds, evicting the
|
|
oldest, so subscribing to 100 feeds cannot grow it without limit.
|
|
*(test: `the_memo_evicts_beyond_its_capacity`.)*
|
|
- [ ] **G6 — `/rss` is in `MUTABLE_ROOTS` in both clients**
|
|
(`cbd-tui/src/rpc.rs`, `cbd-web/src/state.rs`), or the client cache
|
|
defeats G3. *(tests: the existing cacheability tests, extended.)*
|
|
|
|
## Identity and paths
|
|
|
|
- [ ] **G7 — An episode key is stable** across restarts and builds:
|
|
`blake3(guid)[..16]`, and the guid falls back to the enclosure URL only
|
|
when the feed omits one. *(tests: `episode_keys_are_stable_and_short`,
|
|
`a_feed_without_guids_keys_on_the_enclosure_url`.)*
|
|
- [ ] **G8 — Slugs are derived, bounded, and de-duplicated.** Lowercased,
|
|
non-alphanumerics folded to single dashes, trimmed, length-capped; an
|
|
empty or all-punctuation name yields `feed`; two subscriptions that
|
|
slug alike get `-2`, `-3`. *(tests: `slugify_folds_and_bounds`,
|
|
`duplicate_slugs_get_a_suffix`.)*
|
|
- [ ] **G9 — `provider_item_id` is the guid**, so the content store
|
|
de-duplicates captures of the same episode across visits and feeds.
|
|
*(test: `tracks_carry_the_guid_as_provider_item_id`.)*
|
|
- [ ] **G10 — Foreign and malformed paths are typed errors**, never
|
|
panics: an unknown slug, a wrong-provider path, a bad episode key, and
|
|
a path with too many segments all yield `MalformedPath` (or
|
|
`NotSupported` for edits). *(test: `foreign_and_malformed_paths_reject`.)*
|
|
|
|
## Subscriptions (`%`, `e`, `d`)
|
|
|
|
- [ ] **G11 — `%` on `/rss` takes a URL, fetches it once, and names the
|
|
subscription from the feed's own title**, de-duplicating the slug. The
|
|
new node is returned. *(test: `subscribing_names_from_the_feed_title`.)*
|
|
- [ ] **G12 — `%` with a non-URL or an unfetchable URL fails with a typed
|
|
error and adds nothing.** *(test: `subscribing_rejects_bad_input`.)*
|
|
- [ ] **G13 — `e` renames and the path follows the new slug**; `d`
|
|
removes the subscription and no audio. Both return what a client should
|
|
display next. *(tests: `renaming_moves_the_slug`,
|
|
`unsubscribing_removes_only_the_entry`.)*
|
|
- [ ] **G14 — Every subscription change round-trips through `rss.toml`**
|
|
— `settings()` reserializes the live list so the server's write-back
|
|
persists it, and reloading yields the same subscriptions. *(test:
|
|
`subscription_changes_round_trip_through_toml`.)*
|
|
|
|
## Robustness (hard rules)
|
|
|
|
- [ ] **G15 — No panics on feed data.** A missing enclosure, empty title,
|
|
unparseable date, absent duration, or an entry with no guid is skipped
|
|
or degraded — never a panic and never a failed listing. Only an
|
|
unfetchable/unparseable *feed* errors, and it fails that node only.
|
|
*(tests: `malformed_entries_are_skipped`, `backend_failures_are_typed`.)*
|
|
- [ ] **G16 — Bounded network use.** Every request carries
|
|
`call_timeout_secs`; a body over `max_feed_bytes` is rejected without
|
|
being buffered whole; listings are capped at `episodes_per_feed`.
|
|
- [ ] **G17 — Newest first**, by publication date descending where dates
|
|
parse, feed order preserved otherwise (stable sort). *(test:
|
|
`episodes_are_newest_first`.)*
|
|
- [ ] **G18 — A provider failure never takes the server down.** A feed
|
|
that cannot be fetched fails its own node; `init` with no feeds still
|
|
mounts an empty, creatable `/rss`. *(test: `no_feeds_still_mounts`.)*
|
|
|
|
## Wiring
|
|
|
|
- [ ] **G19 — Behind the `rss` cargo feature**, default on: `dep:rssdy`,
|
|
a `BUILT_IN_PROVIDERS` entry, an `ALL_PROVIDERS` name, a
|
|
`ProviderToggles` field, a mount registration, and a `check-features`
|
|
row. The provider-less and each-provider-alone builds stay clean.
|
|
- [ ] **G20 — Documented.** A `docs/src/providers/rss.md` page (tree,
|
|
subscribing, premium feeds and what "not cached" means, every config
|
|
option), an `rssdy/README.md`, links from the providers index,
|
|
`SUMMARY.md`, `config.md`, and the root README's tree and config table.
|
|
- [ ] **G21 — Clippy clean under `-D warnings`** for the workspace, fmt
|
|
clean, and no `todo!()` from the stub stage left.
|