mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
fix(integration): decode persisted settings and align ports
This commit is contained in:
@@ -15,14 +15,12 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tauri_plugin_store::StoreExt;
|
||||
use tokio::sync::watch;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use ts_rs::TS;
|
||||
|
||||
pub const EXTENSION_SERVER_PORT: u16 = 23522;
|
||||
pub const EXTENSION_SERVER_PORT_RANGE: std::ops::RangeInclusive<u16> =
|
||||
EXTENSION_SERVER_PORT..=23531;
|
||||
pub const EXTENSION_SERVER_PORT: u16 = 6412;
|
||||
pub const EXTENSION_SERVER_PORT_RANGE: std::ops::RangeInclusive<u16> = EXTENSION_SERVER_PORT..=6422;
|
||||
const MAX_URL_COUNT: usize = 200;
|
||||
const SIGNATURE_MAX_AGE_MS: u64 = 60_000;
|
||||
const MAIN_QUEUE_ID: &str = "00000000-0000-0000-0000-000000000001";
|
||||
@@ -228,7 +226,7 @@ async fn enqueue_extension_download(
|
||||
app_handle: &AppHandle,
|
||||
download: &ExtensionDownload,
|
||||
) -> Result<(), String> {
|
||||
let Some(settings) = read_settings(app_handle) else {
|
||||
let Ok(settings) = crate::settings::load_settings(app_handle) else {
|
||||
return Err("settings unavailable".to_string());
|
||||
};
|
||||
let state = app_handle.state::<crate::AppState>();
|
||||
@@ -309,13 +307,6 @@ async fn enqueue_extension_download(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_settings(app_handle: &AppHandle) -> Option<crate::ipc::PersistedSettings> {
|
||||
let store = app_handle.store("store.bin").ok()?;
|
||||
let settings_value = store.get("settings")?;
|
||||
let settings_text = settings_value.as_str()?;
|
||||
serde_json::from_str(settings_text).ok()
|
||||
}
|
||||
|
||||
fn merge_headers(referer: Option<&str>, headers: Option<&str>) -> Option<String> {
|
||||
let mut values = Vec::new();
|
||||
if let Some(referer) = referer {
|
||||
@@ -339,7 +330,7 @@ fn filename_from_url(raw_url: &str) -> String {
|
||||
.and_then(|url| {
|
||||
url.path_segments()
|
||||
.and_then(Iterator::last)
|
||||
.and_then(|segment| sanitize_filename(segment))
|
||||
.and_then(sanitize_filename)
|
||||
})
|
||||
.unwrap_or_else(|| "download".to_string())
|
||||
}
|
||||
|
||||
+19
-43
@@ -517,6 +517,7 @@ mod parity;
|
||||
pub mod error;
|
||||
pub mod commands;
|
||||
pub mod retry;
|
||||
mod settings;
|
||||
pub use error::AppError;
|
||||
|
||||
// Retained only for compatibility with the optional aria2 diagnostic monitor.
|
||||
@@ -2153,20 +2154,9 @@ pub fn run() {
|
||||
|
||||
|
||||
let max_concurrent = {
|
||||
use tauri_plugin_store::StoreExt;
|
||||
let mut capacity = crate::queue::DEFAULT_MAX_CONCURRENT;
|
||||
if let Ok(store) = app.handle().store("store.bin") {
|
||||
if let Some(settings_val) = store.get("settings") {
|
||||
if let Some(settings_str) = settings_val.as_str() {
|
||||
if let Ok(settings_json) = serde_json::from_str::<serde_json::Value>(settings_str) {
|
||||
if let Some(n) = settings_json.get("maxConcurrentDownloads").and_then(|v| v.as_u64()) {
|
||||
capacity = n as usize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
capacity
|
||||
crate::settings::load_settings(app.handle())
|
||||
.map(|settings| settings.max_concurrent_downloads)
|
||||
.unwrap_or(crate::queue::DEFAULT_MAX_CONCURRENT)
|
||||
};
|
||||
|
||||
let queue_manager = Arc::new(queue::QueueManager::new(app.handle().clone(), max_concurrent));
|
||||
@@ -2234,21 +2224,9 @@ pub fn run() {
|
||||
}
|
||||
crate::scheduler::spawn_scheduler(app.handle().clone());
|
||||
|
||||
let mut global_speed_limit = String::new();
|
||||
{
|
||||
use tauri_plugin_store::StoreExt;
|
||||
if let Ok(store) = app.handle().store("store.bin") {
|
||||
if let Some(settings_val) = store.get("settings") {
|
||||
if let Some(settings_str) = settings_val.as_str() {
|
||||
if let Ok(settings_json) = serde_json::from_str::<serde_json::Value>(settings_str) {
|
||||
if let Some(limit) = settings_json.get("globalSpeedLimit").and_then(|v| v.as_str()) {
|
||||
global_speed_limit = limit.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let global_speed_limit = crate::settings::load_settings(app.handle())
|
||||
.map(|settings| settings.global_speed_limit)
|
||||
.unwrap_or_default();
|
||||
|
||||
match resolve_bundled_binary_path(app.handle(), "aria2c") {
|
||||
Ok(binary_path) => {
|
||||
@@ -2281,22 +2259,20 @@ pub fn run() {
|
||||
std::thread::spawn(move || {
|
||||
use std::io::BufRead;
|
||||
let reader = std::io::BufReader::new(stderr);
|
||||
for line in reader.lines() {
|
||||
if let Ok(trimmed) = line {
|
||||
let trimmed = trimmed.trim().to_string();
|
||||
if let Ok(mut stderr_lock) = daemon_app.state::<Aria2DaemonGuard>().last_stderr.lock() {
|
||||
stderr_lock.push_str(&trimmed);
|
||||
stderr_lock.push('\n');
|
||||
let excess = stderr_lock.len().saturating_sub(8192);
|
||||
if excess > 0 {
|
||||
let _ = stderr_lock.drain(..excess);
|
||||
}
|
||||
}
|
||||
let lower = trimmed.to_lowercase();
|
||||
if lower.contains("error") || lower.contains("critical") {
|
||||
log::error!("aria2c stderr: {}", trimmed);
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
let trimmed = line.trim().to_string();
|
||||
if let Ok(mut stderr_lock) = daemon_app.state::<Aria2DaemonGuard>().last_stderr.lock() {
|
||||
stderr_lock.push_str(&trimmed);
|
||||
stderr_lock.push('\n');
|
||||
let excess = stderr_lock.len().saturating_sub(8192);
|
||||
if excess > 0 {
|
||||
let _ = stderr_lock.drain(..excess);
|
||||
}
|
||||
}
|
||||
let lower = trimmed.to_lowercase();
|
||||
if lower.contains("error") || lower.contains("critical") {
|
||||
log::error!("aria2c stderr: {}", trimmed);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+15
-16
@@ -257,17 +257,18 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
loop {
|
||||
let debt = self.slots_to_retire.load(Ordering::Relaxed);
|
||||
let to_deduct = std::cmp::min(debt, delta);
|
||||
match self.slots_to_retire.compare_exchange_weak(
|
||||
debt,
|
||||
debt - to_deduct,
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => {
|
||||
delta -= to_deduct;
|
||||
break;
|
||||
}
|
||||
Err(_) => {}
|
||||
if self
|
||||
.slots_to_retire
|
||||
.compare_exchange_weak(
|
||||
debt,
|
||||
debt - to_deduct,
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
delta -= to_deduct;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if delta > 0 {
|
||||
@@ -580,7 +581,7 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
}
|
||||
let payload = payload.unwrap();
|
||||
|
||||
let this = Arc::clone(&self);
|
||||
let this = Arc::clone(self);
|
||||
let stale_gid = gid.to_string();
|
||||
let id_for_task = id.clone();
|
||||
let error_for_emit = error.clone();
|
||||
@@ -890,8 +891,7 @@ impl SidecarSpawner for ProductionSpawner {
|
||||
let mut cancel_rx = state
|
||||
.download_coordinator
|
||||
.register_media(id.to_string())
|
||||
.await
|
||||
.map_err(|e| e)?;
|
||||
.await?;
|
||||
let outcome = crate::start_media_download_internal(
|
||||
self.app_handle.clone(),
|
||||
id,
|
||||
@@ -941,8 +941,7 @@ impl SidecarSpawner for ProductionSpawner {
|
||||
proxy: payload.proxy.clone(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.map_err(|e| e)?;
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+36
-58
@@ -1,8 +1,6 @@
|
||||
use tauri::Emitter;
|
||||
use chrono::{Local, Datelike};
|
||||
use chrono::{Datelike, Local};
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
use tauri::Emitter;
|
||||
|
||||
pub fn spawn_scheduler(app_handle: tauri::AppHandle) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
@@ -10,68 +8,48 @@ pub fn spawn_scheduler(app_handle: tauri::AppHandle) {
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let settings_opt = {
|
||||
use tauri_plugin_store::StoreExt;
|
||||
let store = app_handle.store("store.bin");
|
||||
if let Ok(store) = store {
|
||||
store.get("settings").and_then(|v| v.as_str().map(|s| s.to_string()))
|
||||
} else {
|
||||
None
|
||||
if let Ok(settings) = crate::settings::load_settings(&app_handle) {
|
||||
let scheduler = settings.scheduler;
|
||||
if !scheduler.enabled {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(settings_str) = settings_opt {
|
||||
if let Ok(mut settings) = serde_json::from_str::<serde_json::Value>(&settings_str) {
|
||||
if let Ok(scheduler) = serde_json::from_value::<crate::ipc::SchedulerSettings>(settings.get("scheduler").unwrap_or(&serde_json::json!({})).clone()) {
|
||||
if !scheduler.enabled {
|
||||
continue;
|
||||
}
|
||||
let now = Local::now();
|
||||
let current_time = now.format("%H:%M").to_string();
|
||||
let current_day = now.weekday().num_days_from_sunday();
|
||||
|
||||
let now = Local::now();
|
||||
let current_time = now.format("%H:%M").to_string();
|
||||
let current_day = now.weekday().num_days_from_sunday();
|
||||
let allowed_today =
|
||||
scheduler.everyday || scheduler.selected_days.contains(¤t_day);
|
||||
if !allowed_today {
|
||||
continue;
|
||||
}
|
||||
|
||||
let allowed_today = scheduler.everyday || scheduler.selected_days.contains(¤t_day);
|
||||
if !allowed_today {
|
||||
continue;
|
||||
}
|
||||
let date_key = now.format("%Y-%m-%d").to_string();
|
||||
let trigger_key = format!("{}-{}", date_key, current_time);
|
||||
|
||||
let date_key = now.format("%Y-%m-%d").to_string();
|
||||
let trigger_key = format!("{}-{}", date_key, current_time);
|
||||
if scheduler.start_time == current_time
|
||||
&& settings.scheduler_last_start_key != trigger_key
|
||||
{
|
||||
let key = trigger_key.clone();
|
||||
let _ = crate::settings::update_settings_state(&app_handle, |state| {
|
||||
state.insert("schedulerLastStartKey".to_string(), serde_json::json!(key));
|
||||
state.insert("schedulerRunning".to_string(), serde_json::json!(true));
|
||||
});
|
||||
|
||||
let last_start_key = settings.get("schedulerLastStartKey").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let last_stop_key = settings.get("schedulerLastStopKey").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let _ = app_handle.emit("schedule-trigger", "start");
|
||||
}
|
||||
|
||||
if scheduler.start_time == current_time && last_start_key != trigger_key {
|
||||
settings["schedulerLastStartKey"] = serde_json::json!(trigger_key.clone());
|
||||
settings["schedulerRunning"] = serde_json::json!(true);
|
||||
|
||||
let _ = app_handle.emit("schedule-trigger", "start");
|
||||
|
||||
if let Ok(updated) = serde_json::to_string(&settings) {
|
||||
use tauri_plugin_store::StoreExt;
|
||||
if let Ok(store) = app_handle.store("store.bin") {
|
||||
store.set("settings", serde_json::json!(updated));
|
||||
let _ = store.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
if scheduler.stop_time_enabled
|
||||
&& scheduler.stop_time == current_time
|
||||
&& settings.scheduler_last_stop_key != trigger_key
|
||||
{
|
||||
let key = trigger_key.clone();
|
||||
let _ = crate::settings::update_settings_state(&app_handle, |state| {
|
||||
state.insert("schedulerLastStopKey".to_string(), serde_json::json!(key));
|
||||
state.insert("schedulerRunning".to_string(), serde_json::json!(false));
|
||||
});
|
||||
|
||||
if scheduler.stop_time_enabled && scheduler.stop_time == current_time && last_stop_key != trigger_key {
|
||||
settings["schedulerLastStopKey"] = serde_json::json!(trigger_key.clone());
|
||||
settings["schedulerRunning"] = serde_json::json!(false);
|
||||
|
||||
let _ = app_handle.emit("schedule-trigger", "stop");
|
||||
|
||||
if let Ok(updated) = serde_json::to_string(&settings) {
|
||||
use tauri_plugin_store::StoreExt;
|
||||
if let Ok(store) = app_handle.store("store.bin") {
|
||||
store.set("settings", serde_json::json!(updated));
|
||||
let _ = store.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = app_handle.emit("schedule-trigger", "stop");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
use crate::ipc::{
|
||||
AppFontSize, ListRowDensity, MediaCookieSource, PersistedSettings, PostQueueAction, ProxyMode,
|
||||
SchedulerSettings, SettingsTab, Theme,
|
||||
};
|
||||
use serde_json::{Map, Value};
|
||||
use std::collections::HashMap;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
const SETTINGS_STORE: &str = "store.bin";
|
||||
const SETTINGS_KEY: &str = "settings";
|
||||
|
||||
pub fn load_settings(app_handle: &AppHandle) -> Result<PersistedSettings, String> {
|
||||
let store = app_handle
|
||||
.store(SETTINGS_STORE)
|
||||
.map_err(|error| format!("failed to open settings store: {error}"))?;
|
||||
let stored = store
|
||||
.get(SETTINGS_KEY)
|
||||
.ok_or_else(|| "settings are not persisted yet".to_string())?;
|
||||
decode_stored_settings(&stored)
|
||||
}
|
||||
|
||||
pub fn decode_stored_settings(stored: &Value) -> Result<PersistedSettings, String> {
|
||||
let document = decode_document(stored)?;
|
||||
let state = settings_state(&document)?;
|
||||
let mut merged = serde_json::to_value(default_settings())
|
||||
.map_err(|error| format!("failed to serialize settings defaults: {error}"))?;
|
||||
merge_json(&mut merged, state);
|
||||
|
||||
let mut settings: PersistedSettings = serde_json::from_value(merged)
|
||||
.map_err(|error| format!("invalid persisted settings: {error}"))?;
|
||||
validate_settings(&mut settings);
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
pub fn update_settings_state(
|
||||
app_handle: &AppHandle,
|
||||
update: impl FnOnce(&mut Map<String, Value>),
|
||||
) -> Result<(), String> {
|
||||
let store = app_handle
|
||||
.store(SETTINGS_STORE)
|
||||
.map_err(|error| format!("failed to open settings store: {error}"))?;
|
||||
let stored = store
|
||||
.get(SETTINGS_KEY)
|
||||
.ok_or_else(|| "settings are not persisted yet".to_string())?;
|
||||
let was_string = stored.is_string();
|
||||
let mut document = decode_document(&stored)?;
|
||||
update(settings_state_mut(&mut document)?);
|
||||
|
||||
let stored = if was_string {
|
||||
Value::String(
|
||||
serde_json::to_string(&document)
|
||||
.map_err(|error| format!("failed to encode settings: {error}"))?,
|
||||
)
|
||||
} else {
|
||||
document
|
||||
};
|
||||
store.set(SETTINGS_KEY, stored);
|
||||
store
|
||||
.save()
|
||||
.map_err(|error| format!("failed to save settings: {error}"))
|
||||
}
|
||||
|
||||
fn decode_document(stored: &Value) -> Result<Value, String> {
|
||||
match stored {
|
||||
Value::String(text) => serde_json::from_str(text)
|
||||
.map_err(|error| format!("failed to parse persisted settings: {error}")),
|
||||
Value::Object(_) => Ok(stored.clone()),
|
||||
_ => Err("persisted settings must be a JSON string or object".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn settings_state(document: &Value) -> Result<&Value, String> {
|
||||
if let Some(state) = document.get("state") {
|
||||
if !state.is_object() {
|
||||
return Err("persisted settings state must be an object".to_string());
|
||||
}
|
||||
Ok(state)
|
||||
} else if document.is_object() {
|
||||
Ok(document)
|
||||
} else {
|
||||
Err("persisted settings must be an object".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn settings_state_mut(document: &mut Value) -> Result<&mut Map<String, Value>, String> {
|
||||
let has_envelope = document.get("state").is_some();
|
||||
let state = if has_envelope {
|
||||
document
|
||||
.get_mut("state")
|
||||
.ok_or_else(|| "persisted settings state is missing".to_string())?
|
||||
} else {
|
||||
document
|
||||
};
|
||||
state
|
||||
.as_object_mut()
|
||||
.ok_or_else(|| "persisted settings state must be an object".to_string())
|
||||
}
|
||||
|
||||
fn merge_json(target: &mut Value, source: &Value) {
|
||||
match (target, source) {
|
||||
(Value::Object(target), Value::Object(source)) => {
|
||||
for (key, value) in source {
|
||||
if let Some(target_value) = target.get_mut(key) {
|
||||
merge_json(target_value, value);
|
||||
} else {
|
||||
target.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
(target, source) => *target = source.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_settings(settings: &mut PersistedSettings) {
|
||||
if settings.max_concurrent_downloads == 0 {
|
||||
settings.max_concurrent_downloads = default_settings().max_concurrent_downloads;
|
||||
}
|
||||
}
|
||||
|
||||
fn default_settings() -> PersistedSettings {
|
||||
let download_directories = [
|
||||
("Musics", "~/Downloads/Musics"),
|
||||
("Movies", "~/Downloads/Movies"),
|
||||
("Compressed", "~/Downloads/Compressed"),
|
||||
("Documents", "~/Downloads/Documents"),
|
||||
("Pictures", "~/Downloads/Pictures"),
|
||||
("Applications", "~/Downloads/Applications"),
|
||||
("Other", "~/Downloads/Other"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(category, path)| (category.to_string(), path.to_string()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
PersistedSettings {
|
||||
theme: Theme::System,
|
||||
default_download_path: "~/Downloads".to_string(),
|
||||
max_concurrent_downloads: 3,
|
||||
global_speed_limit: String::new(),
|
||||
is_sidebar_visible: true,
|
||||
active_settings_tab: SettingsTab::Downloads,
|
||||
scheduler: SchedulerSettings {
|
||||
enabled: false,
|
||||
start_time: "00:00".to_string(),
|
||||
stop_time_enabled: false,
|
||||
stop_time: "08:00".to_string(),
|
||||
everyday: true,
|
||||
selected_days: vec![0, 1, 2, 3, 4, 5, 6],
|
||||
post_queue_action: PostQueueAction::None,
|
||||
},
|
||||
scheduler_last_start_key: String::new(),
|
||||
scheduler_last_stop_key: String::new(),
|
||||
last_custom_speed_limit_ki_b: 1024,
|
||||
per_server_connections: 16,
|
||||
max_automatic_retries: 3,
|
||||
show_notifications: true,
|
||||
play_completion_sound: true,
|
||||
app_font_size: AppFontSize::Standard,
|
||||
list_row_density: ListRowDensity::Standard,
|
||||
show_dock_badge: true,
|
||||
show_menu_bar_icon: true,
|
||||
proxy_mode: ProxyMode::None,
|
||||
proxy_host: String::new(),
|
||||
proxy_port: 8080,
|
||||
custom_user_agent: String::new(),
|
||||
ask_where_to_save_each_file: false,
|
||||
prevents_sleep_while_downloading: true,
|
||||
media_cookie_source: MediaCookieSource::None,
|
||||
download_directories,
|
||||
site_logins: Vec::new(),
|
||||
extension_pairing_token: String::new(),
|
||||
auto_check_updates: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::decode_stored_settings;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[test]
|
||||
fn decodes_zustand_envelope_and_preserves_non_default_startup_settings() {
|
||||
let stored = json!({
|
||||
"state": {
|
||||
"maxConcurrentDownloads": 7,
|
||||
"globalSpeedLimit": "2M",
|
||||
"scheduler": {
|
||||
"enabled": true,
|
||||
"startTime": "06:30",
|
||||
"stopTimeEnabled": true,
|
||||
"stopTime": "23:15",
|
||||
"everyday": false,
|
||||
"selectedDays": [1, 3, 5],
|
||||
"postQueueAction": "sleep"
|
||||
}
|
||||
},
|
||||
"version": 0
|
||||
});
|
||||
|
||||
let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap();
|
||||
|
||||
assert_eq!(settings.max_concurrent_downloads, 7);
|
||||
assert_eq!(settings.global_speed_limit, "2M");
|
||||
assert!(settings.scheduler.enabled);
|
||||
assert_eq!(settings.scheduler.start_time, "06:30");
|
||||
assert_eq!(settings.scheduler.selected_days, vec![1, 3, 5]);
|
||||
assert_eq!(settings.default_download_path, "~/Downloads");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_legacy_top_level_settings() {
|
||||
let stored = json!({
|
||||
"maxConcurrentDownloads": 5,
|
||||
"globalSpeedLimit": "512K"
|
||||
});
|
||||
|
||||
let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap();
|
||||
|
||||
assert_eq!(settings.max_concurrent_downloads, 5);
|
||||
assert_eq!(settings.global_speed_limit, "512K");
|
||||
assert!(!settings.scheduler.enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replaces_zero_concurrency_with_the_safe_default() {
|
||||
let stored = json!({"state": {"maxConcurrentDownloads": 0}, "version": 0});
|
||||
|
||||
let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap();
|
||||
|
||||
assert_eq!(settings.max_concurrent_downloads, 3);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user