Retention follows per-user read and starred

reap_candidates still read entries.read/flagged, which nothing writes
since read state moved to entry_state -- so starring no longer protected
a file and the read-first ordering was dead. One file serves every
subscriber, so anyone starring it keeps it, and it counts as read only
once everyone subscribed has read it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
2026-09-11 02:25:24 +00:00
parent d46ec73261
commit 7df4ee7dde
3 changed files with 59 additions and 20 deletions

View File

@@ -150,22 +150,39 @@ mod tests {
}
#[test]
fn query_never_offers_flagged_files_and_prefers_read_ones() {
fn query_never_offers_a_file_anyone_starred_and_prefers_ones_everyone_read() {
// One file serves both subscribers, so it takes both of them to release it.
let db = Db::memory().unwrap();
db.exec_for_test(
"INSERT INTO entries (feed_id, guid, first_seen, read, flagged) VALUES
('f', 'keep', 0, 1, 1),
('f', 'unread', 0, 0, 0),
('f', 'read', 0, 1, 0);
"INSERT INTO users (id, name, is_admin, created) VALUES (1,'ray',1,0),(2,'sam',0,0);
INSERT INTO subscriptions (user_id, feed_id, created) VALUES (1,'f',0),(2,'f',0);
INSERT INTO entries (feed_id, guid, first_seen) VALUES
('f', 'keep', 0),
('f', 'half', 0),
('f', 'unread', 0),
('f', 'read', 0);
-- Starred by one of the two, so it stays whatever the other thinks.
INSERT INTO entry_state (user_id, feed_id, guid, read, flagged) VALUES
(1, 'f', 'keep', 1, 1),
(2, 'f', 'keep', 1, 0),
(1, 'f', 'half', 1, 0),
(1, 'f', 'read', 1, 0),
(2, 'f', 'read', 1, 0);
INSERT INTO enclosures (id, feed_id, guid, url, path, bytes_done, state, downloaded_at) VALUES
(1, 'f', 'keep', 'u1', '/tmp/keep', 10, 'done', 10),
(2, 'f', 'unread', 'u2', '/tmp/unread', 10, 'done', 20),
(3, 'f', 'read', 'u3', '/tmp/read', 10, 'done', 30);",
(2, 'f', 'half', 'u2', '/tmp/half', 10, 'done', 20),
(3, 'f', 'unread', 'u3', '/tmp/unread', 10, 'done', 30),
(4, 'f', 'read', 'u4', '/tmp/read', 10, 'done', 40);",
)
.unwrap();
let got: Vec<i64> = db.reap_candidates().unwrap().iter().map(|c| c.id).collect();
assert_eq!(got, vec![3, 2], "flagged excluded; read goes before unread");
assert_eq!(
got,
vec![4, 2, 3],
"starred by anyone is never offered; read by everyone goes first, and one \
person still having it unread keeps it back with the unread ones"
);
}
#[test]