feat: add per-user retention settings to users.toml

New [user.retention] TOML sub-block with auto_close_days and
auto_delete_days. Both default to 0, which disables the respective
sweep — a conservative default that preserves current behaviour for
existing deployments and lets admins test auto-close in isolation.

Only parsing + the test User literals are touched here; the actual
lazy sweep consuming these values lands in the next commit.
This commit is contained in:
2026-04-21 10:48:24 +02:00
parent 8173ff2f26
commit e5a62d8f10
14 changed files with 90 additions and 0 deletions
+71
View File
@@ -12,6 +12,20 @@ pub struct WhisperUserSettings {
pub initial_prompt: Option<String>,
}
/// Per-user retention settings from users.toml.
///
/// `auto_close_days = 0` disables the auto-close sweep (a case is never
/// closed for age alone). `auto_delete_days = 0` disables the auto-purge
/// sweep (closed cases stay forever until manual purge) — important for
/// admin / dev setups where data must survive across restarts.
#[derive(Debug, Default, Clone, Deserialize)]
pub struct RetentionUserSettings {
#[serde(default)]
pub auto_close_days: u32,
#[serde(default)]
pub auto_delete_days: u32,
}
/// A single user entry from users.toml.
#[derive(Debug, Deserialize)]
pub struct User {
@@ -21,6 +35,8 @@ pub struct User {
pub role: String,
#[serde(default)]
pub whisper: WhisperUserSettings,
#[serde(default)]
pub retention: RetentionUserSettings,
}
/// Wrapper for deserializing the [[user]] array from TOML.
@@ -258,6 +274,7 @@ mod tests {
web_password: String::new(),
role: "doctor".into(),
whisper: WhisperUserSettings::default(),
retention: RetentionUserSettings::default(),
}
}
@@ -277,6 +294,60 @@ mod tests {
assert!(u.whisper.initial_prompt.is_none());
}
#[test]
fn parses_user_without_retention_block_defaults_to_zero() {
let toml_str = r#"
[[user]]
slug = "a"
api_key = "k1"
web_password = ""
role = "doctor"
"#;
let parsed: UsersFile = toml::from_str(toml_str).unwrap();
let r = &parsed.user[0].retention;
// Both zero means "feature disabled", matching the pre-migration
// status quo — no case is ever auto-closed or auto-purged.
assert_eq!(r.auto_close_days, 0);
assert_eq!(r.auto_delete_days, 0);
}
#[test]
fn parses_user_with_retention_block() {
let toml_str = r#"
[[user]]
slug = "a"
api_key = "k1"
web_password = ""
role = "doctor"
[user.retention]
auto_close_days = 7
auto_delete_days = 30
"#;
let parsed: UsersFile = toml::from_str(toml_str).unwrap();
let r = &parsed.user[0].retention;
assert_eq!(r.auto_close_days, 7);
assert_eq!(r.auto_delete_days, 30);
}
#[test]
fn parses_user_with_partial_retention_block() {
let toml_str = r#"
[[user]]
slug = "a"
api_key = "k1"
web_password = ""
role = "doctor"
[user.retention]
auto_close_days = 5
"#;
let parsed: UsersFile = toml::from_str(toml_str).unwrap();
let r = &parsed.user[0].retention;
assert_eq!(r.auto_close_days, 5);
// Missing field defaults to 0 = disabled — lets admins test auto-close
// in isolation without losing data to auto-purge.
assert_eq!(r.auto_delete_days, 0);
}
#[test]
fn parses_user_with_full_whisper_block() {
let toml_str = r#"