crabidy/quality/rss-provider.md

5.5 KiB

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.tomlsettings() 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.