Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions crates/codexbar-app/src/ui_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2692,6 +2692,40 @@ fn minute_ticks_refresh_compact_history_and_smart_order_without_a_fetch(cx: &mut
);
}

#[gpui_kit::test]
fn a_refresh_interval_too_long_for_a_date_waits_instead_of_panicking(cx: &mut TestAppContext) {
// settings.json can be edited by hand: an interval no date can hold still shows and ticks, and doesn't fetch again.
let settings = TempSettings::new(
"huge-refresh-interval",
r#"{ "refreshIntervalSeconds": 9000000000000000000 }"#,
);
let base = chrono::Utc::now();
let account = AccountSnapshot::new(
AccountId::new("claude-huge-interval"),
Provider::Claude,
vec![Metric::Window {
label: "Weekly".into(),
used: 0.4,
resets_at: base + chrono::Duration::days(3),
pace: None,
length: None,
}],
base,
);
let dashboard = open_live_fixed(cx, &settings, vec![Arc::new(FixedProvider(vec![account]))]);
cx.run_until_parked();
let fetched = cx.update(|cx| dashboard.read(cx).last_refresh_for_test());
assert!(fetched.is_some(), "the first refresh runs");
cx.executor().advance_clock(Duration::from_secs(2));
cx.run_until_parked();
assert_eq!(shown_ids(cx, &dashboard), ["claude-huge-interval"]);
assert_eq!(
cx.update(|cx| dashboard.read(cx).last_refresh_for_test()),
fetched,
"no second fetch inside the interval"
);
}

#[gpui_kit::test]
fn smart_order_puts_urgent_accounts_first_and_manual_comes_back(cx: &mut TestAppContext) {
let settings = TempSettings::new("smart-order", "{}");
Expand Down
34 changes: 32 additions & 2 deletions crates/codexbar-store/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const LOCK_FILE: &str = "settings.write.lock";
/// Zoom range and step shared with the WPF app (`ZoomHelper`).
pub const MIN_ZOOM: f64 = 0.5;
pub const MAX_ZOOM: f64 = 3.0;
/// The longest automatic refresh interval: a year, as the longest Retry-After a provider backoff honors.
pub const MAX_REFRESH_INTERVAL_SECS: u64 = 365 * 24 * 3600;
pub const ZOOM_STEP: f64 = 0.1;

/// Provider names as the WPF app stores them (`ProviderId` enum names).
Expand Down Expand Up @@ -463,11 +465,13 @@ impl Settings {
.unwrap_or_default()
}

/// Refresh interval in seconds; `None` means automatic refresh is off.
/// Refresh interval in seconds; `None` means automatic refresh is off. A longer interval than
/// [`MAX_REFRESH_INTERVAL_SECS`] (only a hand edit writes one) reads as that year, so the next refresh is
/// still a date the dashboard can count down to; the file keeps what it holds.
pub fn refresh_interval_secs(&self) -> Option<u64> {
match self.doc.get("refreshIntervalSeconds").and_then(Value::as_i64) {
Some(secs) if secs <= 0 => None,
Some(secs) => Some(secs as u64),
Some(secs) => Some(secs.unsigned_abs().min(MAX_REFRESH_INTERVAL_SECS)),
None => Some(120),
}
}
Expand Down Expand Up @@ -1438,6 +1442,32 @@ mod tests {
assert_eq!(settings.refresh_interval_secs(), None);
}

#[test]
fn refresh_interval_past_a_year_reads_as_a_year_and_stays_in_the_file() {
let year = MAX_REFRESH_INTERVAL_SECS;
for (stored, expected) in [
(3600, 3600),
(year as i64, year),
(year as i64 + 1, year),
(i64::MAX, year),
] {
let settings = Settings::from_json(&format!(r#"{{"refreshIntervalSeconds": {stored}}}"#));
assert_eq!(settings.refresh_interval_secs(), Some(expected), "{stored}");
}
// Whatever it reads as, the next refresh is a date the dashboard can count down to.
let interval = chrono::Duration::try_seconds(i64::try_from(year).unwrap()).unwrap();
assert!(chrono::Utc::now().checked_add_signed(interval).is_some());
// Reading never rewrites the hand-edited value; another setting's save keeps it.
let dir = TempDir::with(PRIMARY_FILE, r#"{"refreshIntervalSeconds": 9000000000000000000}"#);
let mut settings = Settings::load(&dir.0).unwrap();
settings.set_zoom_level(1.5);
settings.save().unwrap();
assert_eq!(
dir.read(PRIMARY_FILE)["refreshIntervalSeconds"],
json!(9_000_000_000_000_000_000i64)
);
}

#[test]
fn load_missing_file_applies_wpf_defaults() {
let dir = TempDir::with(PRIMARY_FILE, "");
Expand Down
Loading