feat(sync): include plugin settings in snapshots

This commit is contained in:
2026-05-16 07:12:52 -04:00
parent 80729083eb
commit 7823ecd0a3
6 changed files with 230 additions and 0 deletions
+1
View File
@@ -34,6 +34,7 @@ mod sync_apply;
mod sync_context; mod sync_context;
mod sync_history; mod sync_history;
mod sync_notes; mod sync_notes;
mod sync_plugin_settings;
mod sync_profiles; mod sync_profiles;
mod sync_reading_list; mod sync_reading_list;
mod sync_site_permissions; mod sync_site_permissions;
@@ -51,6 +51,18 @@ impl InstalledPlugin {
self.private_window_allowed = allowed; self.private_window_allowed = allowed;
} }
pub(super) fn apply_synced_settings(
&mut self,
enabled: bool,
private_window_allowed: bool,
) -> bool {
let changed =
self.enabled != enabled || self.private_window_allowed != private_window_allowed;
self.enabled = enabled;
self.private_window_allowed = private_window_allowed;
changed
}
#[must_use] #[must_use]
pub fn manifest(&self) -> &PluginManifest { pub fn manifest(&self) -> &PluginManifest {
&self.manifest &self.manifest
@@ -34,6 +34,9 @@ impl BrowserCore {
for record in body.history { for record in body.history {
self.apply_history_sync_record(record, &mut summary, &context)?; self.apply_history_sync_record(record, &mut summary, &context)?;
} }
for record in body.plugin_settings {
self.apply_plugin_settings_sync_record(record, &mut summary)?;
}
Ok(summary) Ok(summary)
} }
} }
@@ -0,0 +1,38 @@
use ely_domain::{PluginId, SyncObjectKind, SyncObjectPolicy};
use ely_sync_client::SyncClientError;
use super::{BrowserCore, InstalledPlugin, sync::snapshot_schema_error};
use crate::{sync_engine::SyncSnapshotApplySummary, sync_records::PluginSettingsSyncRecord};
impl BrowserCore {
pub(crate) fn visible_plugin_settings_for_sync(&self) -> Vec<&InstalledPlugin> {
if self.sync_object_policy(SyncObjectKind::PluginSettings) == SyncObjectPolicy::Paused {
return Vec::new();
}
self.installed_plugins.iter().collect()
}
pub(super) fn apply_plugin_settings_sync_record(
&mut self,
record: PluginSettingsSyncRecord,
summary: &mut SyncSnapshotApplySummary,
) -> Result<(), SyncClientError> {
let plugin_id = PluginId::parse(&record.plugin_id).map_err(snapshot_schema_error)?;
let Some(plugin) =
self.installed_plugins.iter_mut().find(|plugin| plugin.id() == &plugin_id)
else {
summary.record_skipped();
return Ok(());
};
if plugin.manifest().checksum() != record.checksum {
summary.record_skipped();
return Ok(());
}
if plugin.apply_synced_settings(record.enabled, record.private_window_allowed) {
summary.record_updated();
} else {
summary.record_skipped();
}
Ok(())
}
}
@@ -8,6 +8,7 @@ use ely_domain::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::state::BrowserCore; use crate::state::BrowserCore;
use crate::state::InstalledPlugin;
pub(crate) const SNAPSHOT_SCHEMA_REV: u32 = 1; pub(crate) const SNAPSHOT_SCHEMA_REV: u32 = 1;
@@ -29,6 +30,8 @@ pub(crate) struct SyncSnapshotBody {
pub(crate) site_permissions: Vec<SitePermissionSyncRecord>, pub(crate) site_permissions: Vec<SitePermissionSyncRecord>,
#[serde(default)] #[serde(default)]
pub(crate) history: Vec<HistorySyncRecord>, pub(crate) history: Vec<HistorySyncRecord>,
#[serde(default)]
pub(crate) plugin_settings: Vec<PluginSettingsSyncRecord>,
} }
impl SyncSnapshotBody { impl SyncSnapshotBody {
@@ -91,6 +94,11 @@ impl SyncSnapshotBody {
HistorySyncRecord::from_entry(entry, core.sync_space_name_for(entry.space_id())) HistorySyncRecord::from_entry(entry, core.sync_space_name_for(entry.space_id()))
}) })
.collect(), .collect(),
plugin_settings: core
.visible_plugin_settings_for_sync()
.into_iter()
.map(PluginSettingsSyncRecord::from_plugin)
.collect(),
} }
} }
} }
@@ -440,6 +448,25 @@ impl HistorySyncRecord {
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct PluginSettingsSyncRecord {
pub(crate) plugin_id: String,
pub(crate) checksum: String,
pub(crate) enabled: bool,
pub(crate) private_window_allowed: bool,
}
impl PluginSettingsSyncRecord {
fn from_plugin(plugin: &InstalledPlugin) -> Self {
Self {
plugin_id: plugin.id().as_str().to_string(),
checksum: plugin.manifest().checksum().to_string(),
enabled: plugin.enabled(),
private_window_allowed: plugin.private_window_allowed(),
}
}
}
fn default_sync_enabled() -> bool { fn default_sync_enabled() -> bool {
true true
} }
@@ -0,0 +1,149 @@
use std::error::Error;
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
use ely_domain::{PluginManifest, SyncObjectKind, SyncObjectPolicy};
#[test]
fn sync_snapshot_updates_existing_plugin_settings() -> Result<(), Box<dyn Error>> {
let mut source = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let plugin_id =
source.install_plugin(plugin_manifest("com.elydora.reader", DEFAULT_CHECKSUM)?, false)?;
source.set_plugin_private_window_allowed(&plugin_id, true)?;
source.disable_plugin(&plugin_id)?;
pause_tab_sync(&mut source);
let bytes = source.build_sync_snapshot_bytes()?;
let mut target = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let target_plugin_id =
target.install_plugin(plugin_manifest("com.elydora.reader", DEFAULT_CHECKSUM)?, false)?;
let summary = target.apply_sync_snapshot_bytes(&bytes)?;
let snapshot = target.snapshot()?;
let [plugin] = snapshot.installed_plugins.as_slice() else {
return Err(format!(
"expected 1 installed plugin, got {}",
snapshot.installed_plugins.len()
)
.into());
};
assert_eq!(summary.imported(), 0);
assert_eq!(summary.updated(), 1);
assert_eq!(summary.skipped(), 0);
assert_eq!(plugin.id(), &target_plugin_id);
assert!(!plugin.enabled());
assert!(plugin.private_window_allowed());
assert_eq!(snapshot.plugin_audit_events.len(), 1);
Ok(())
}
#[test]
fn sync_snapshot_omits_paused_plugin_settings() -> Result<(), Box<dyn Error>> {
let mut source = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let plugin_id =
source.install_plugin(plugin_manifest("com.elydora.reader", DEFAULT_CHECKSUM)?, false)?;
source.disable_plugin(&plugin_id)?;
source.set_sync_object_policy(SyncObjectKind::PluginSettings, SyncObjectPolicy::Paused);
pause_tab_sync(&mut source);
let bytes = source.build_sync_snapshot_bytes()?;
let mut target = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
target.install_plugin(plugin_manifest("com.elydora.reader", DEFAULT_CHECKSUM)?, false)?;
let summary = target.apply_sync_snapshot_bytes(&bytes)?;
let snapshot = target.snapshot()?;
let [plugin] = snapshot.installed_plugins.as_slice() else {
return Err(format!(
"expected 1 installed plugin, got {}",
snapshot.installed_plugins.len()
)
.into());
};
assert_eq!(summary.imported(), 0);
assert_eq!(summary.updated(), 0);
assert_eq!(summary.skipped(), 0);
assert!(plugin.enabled());
assert!(!plugin.private_window_allowed());
Ok(())
}
#[test]
fn sync_snapshot_skips_missing_plugin_package() -> Result<(), Box<dyn Error>> {
let mut source = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let plugin_id =
source.install_plugin(plugin_manifest("com.elydora.reader", DEFAULT_CHECKSUM)?, false)?;
source.disable_plugin(&plugin_id)?;
pause_tab_sync(&mut source);
let bytes = source.build_sync_snapshot_bytes()?;
let mut target = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let summary = target.apply_sync_snapshot_bytes(&bytes)?;
let snapshot = target.snapshot()?;
assert_eq!(summary.imported(), 0);
assert_eq!(summary.updated(), 0);
assert_eq!(summary.skipped(), 1);
assert!(snapshot.installed_plugins.is_empty());
Ok(())
}
#[test]
fn sync_snapshot_skips_checksum_mismatched_plugin_package() -> Result<(), Box<dyn Error>> {
let mut source = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let plugin_id =
source.install_plugin(plugin_manifest("com.elydora.reader", DEFAULT_CHECKSUM)?, false)?;
source.disable_plugin(&plugin_id)?;
pause_tab_sync(&mut source);
let bytes = source.build_sync_snapshot_bytes()?;
let mut target = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
target.install_plugin(plugin_manifest("com.elydora.reader", OTHER_CHECKSUM)?, false)?;
let summary = target.apply_sync_snapshot_bytes(&bytes)?;
let snapshot = target.snapshot()?;
let [plugin] = snapshot.installed_plugins.as_slice() else {
return Err(format!(
"expected 1 installed plugin, got {}",
snapshot.installed_plugins.len()
)
.into());
};
assert_eq!(summary.imported(), 0);
assert_eq!(summary.updated(), 0);
assert_eq!(summary.skipped(), 1);
assert!(plugin.enabled());
assert_eq!(plugin.manifest().checksum(), OTHER_CHECKSUM.to_ascii_lowercase());
Ok(())
}
const DEFAULT_CHECKSUM: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
const OTHER_CHECKSUM: &str = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
fn plugin_manifest(id: &str, checksum: &str) -> Result<PluginManifest, Box<dyn Error>> {
PluginManifest::from_toml(plugin_manifest_toml(id, checksum).as_str()).map_err(Into::into)
}
fn pause_tab_sync(core: &mut BrowserCore) {
core.set_sync_object_policy(SyncObjectKind::Tabs, SyncObjectPolicy::Paused);
}
fn plugin_manifest_toml(id: &str, checksum: &str) -> String {
format!(
r#"
id = "{id}"
name = "Reader Exporter"
description = "Exports the active reader view."
author = "Elydora"
homepage = "https://elydora.com/plugins/reader"
permissions = ["page:metadata", "ui:command"]
contributes = ["command-bar-command"]
min_ely_build = "0.1.0"
checksum = "{checksum}"
[signature]
algorithm = "ed25519"
key_id = "elydora-alpha-plugins"
public_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
value = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
"#
)
}