// crates/qbz-app/src/settings/bundle/tests.rs — the normative test suite for // the settings portability engine. Per 04-settings-portability.md, "the // classification table IS the test suite": one test per §3/§5 rule. The engine // takes a `LiveSystem` by injection, so nothing here touches audio hardware. use super::*; use serde_json::json; // ---------------------------------- fixtures ---------------------------------- fn scratch(name: &str) -> ProfilePaths { let nonce = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos(); let base = std::env::temp_dir().join(format!( "qbz-bundle-{name}-{}-{nonce}", std::process::id() )); ProfilePaths { config_root: base.join("config"), data_root: base.join("data"), } } fn cleanup(p: &ProfilePaths) { let _ = std::fs::remove_dir_all(p.data_root.parent().unwrap_or(&p.data_root)); } fn live() -> LiveSystem { LiveSystem { backends: vec!["SystemDefault".into(), "Alsa".into(), "PipeWire".into()], devices: vec![ ("hw:1,0".into(), "Topping D90".into()), ("hw:0,0".into(), "Onboard".into()), ], } } fn bundle_with(domains: serde_json::Value) -> Bundle { let obj = domains.as_object().cloned().unwrap_or_default(); Bundle { schema_version: SCHEMA_VERSION, created_at: "2026-07-14T09:30:00Z".into(), source: BundleSource { app_version: "2.0.2".into(), profile: "desktop".into(), hostname: "workstation".into(), }, domains: obj, } } fn find(lines: &[PlanLine], key: &str) -> Option { lines.iter().find(|l| l.key == key).cloned() } fn find_contains(lines: &[PlanLine], needle: &str) -> Option { lines.iter().find(|l| l.key.contains(needle)).cloned() } fn write_of<'a>(plan: &'a ImportPlan, key: &str) -> Option<&'a Value> { plan.writes.iter().find(|(k, _)| k == key).map(|(_, v)| v) } // ------------------------------- the rules ------------------------------- #[test] fn portable_fields_apply_verbatim() { // §3 PORTABLE: playback.*, the audio portable subset, prefs.streaming_quality. let p = scratch("portable"); let bundle = bundle_with(json!({ "playback": { "autoplay_mode": "infinite", "show_context_icon": false, "persist_session": true, "resume_playback_position": false }, "audio": { "gapless_enabled": true, "stream_buffer_seconds": 4, "normalization_target_lufs": -18.0, "sync_audio_on_startup": true }, "prefs": { "streaming_quality": "hires_plus" } })); let plan = plan(&bundle, &p, &ImportOptions::default(), &live()).expect("plan"); assert!(plan.adapted.is_empty(), "portable fields must not adapt: {:?}", plan.adapted); assert_eq!(find(&plan.applied, "playback.autoplay_mode").unwrap().new, "infinite"); assert_eq!(find(&plan.applied, "playback.show_context_icon").unwrap().new, "false"); assert_eq!(find(&plan.applied, "audio.gapless_enabled").unwrap().new, "true"); assert_eq!(find(&plan.applied, "audio.stream_buffer_seconds").unwrap().new, "4"); assert_eq!(find(&plan.applied, "prefs.streaming_quality").unwrap().new, "hires_plus"); cleanup(&p); } #[test] fn volume_is_never_class_even_hand_added() { // §1 corollary: a hand-edited bundle with `volume` anywhere -> skipped, always. let p = scratch("volume"); let bundle = bundle_with(json!({ "audio": { "volume": 0.8, "gapless_enabled": true }, "prefs": { "streaming_quality": "cd", "volume": 0.9 } })); let plan = plan(&bundle, &p, &ImportOptions::default(), &live()).expect("plan"); let a = find(&plan.skipped, "audio.volume").expect("audio.volume skipped"); assert!(a.why.contains("never imported"), "{}", a.why); let pr = find(&plan.skipped, "prefs.volume").expect("prefs.volume skipped"); assert!(pr.why.contains("never imported"), "{}", pr.why); // No write ever carries a volume. assert!(plan.writes.iter().all(|(k, _)| !k.contains("volume"))); cleanup(&p); } #[test] fn device_uuid_never_imported() { // §2.4: cloning the QConnect identity makes two nodes fight → NEVER. let p = scratch("uuid"); let bundle = bundle_with(json!({ "qconnect": { "device_uuid": "abc-123", "device_name": "Studio" } })); let plan = plan(&bundle, &p, &ImportOptions::default(), &live()).expect("plan"); assert!(find(&plan.skipped, "qconnect.device_uuid").is_some()); // device_name is still applied verbatim. assert_eq!(find(&plan.applied, "qconnect.device_name").unwrap().new, "Studio"); assert!(plan.writes.iter().all(|(k, _)| k != "qconnect.device_uuid")); cleanup(&p); } #[test] fn dsd_downgrades_without_trust_flag() { // §5.3 step 4: dop/native → convert unless --trust-dsd (current is convert, // so this is a CHANGE — the no-change short-circuit does not fire). let p = scratch("dsd"); let bundle = bundle_with(json!({ "audio": { "dsd_mode": "dop" } })); let plan_no_trust = plan(&bundle, &p, &ImportOptions::default(), &live()).expect("plan"); let line = find(&plan_no_trust.adapted, "audio.dsd_mode").expect("dsd adapted"); assert_eq!(line.old.as_deref(), Some("dop")); assert_eq!(line.new, "convert"); assert_eq!(write_of(&plan_no_trust, "audio.dsd_mode"), Some(&json!("convert"))); let opts = ImportOptions { trust_dsd: true, ..Default::default() }; let plan_trust = plan(&bundle, &p, &opts, &live()).expect("plan"); assert!(find(&plan_trust.adapted, "audio.dsd_mode").is_none()); assert_eq!(find(&plan_trust.applied, "audio.dsd_mode").unwrap().new, "dop"); cleanup(&p); } #[test] fn ask_maps_to_always_fallback_in_adapted() { // §5.5: "ask" needs a UI the daemon lacks → always_fallback, in adapted, // never a silent skip. let p = scratch("ask"); let bundle = bundle_with(json!({ "audio": { "quality_fallback_behavior": "ask" } })); let plan = plan(&bundle, &p, &ImportOptions::default(), &live()).expect("plan"); let line = find(&plan.adapted, "audio.quality_fallback_behavior").expect("adapted"); assert_eq!(line.old.as_deref(), Some("ask")); assert_eq!(line.new, "always_fallback"); assert!(find(&plan.skipped, "audio.quality_fallback_behavior").is_none()); cleanup(&p); } #[test] fn unknown_field_skipped_never_error() { // §5.3 step 3 / §7: unknown keys → skipped, never an error. let p = scratch("unknown"); let bundle = bundle_with(json!({ "audio": { "some_future_flag": true }, "brand_new_domain": { "x": 1 } })); let plan = plan(&bundle, &p, &ImportOptions::default(), &live()).expect("must not error"); let a = find(&plan.skipped, "audio.some_future_flag").expect("unknown audio key skipped"); assert!(a.why.contains("unknown field"), "{}", a.why); let d = find(&plan.skipped, "brand_new_domain").expect("unknown domain skipped"); assert!(d.why.contains("unknown field"), "{}", d.why); cleanup(&p); } #[test] fn secrets_double_gate() { // §3/§6: secrets present but no import-side --include-auth → skipped, and the // auth token is NOT queued for validation. let p = scratch("secrets"); // A user must exist for the scrobbler secret to reach the gate (else §5.7 // no-user skip fires first). std::fs::create_dir_all(&p.data_root).unwrap(); write_last_user_id(&p.data_root, 1234567).unwrap(); let bundle = bundle_with(json!({ "integrations": { "scrobblers": { "lastfm_session_key": "d580secret" } }, "auth": { "user_auth_token": "Bo4Asecret", "user_id": 1234567 } })); let plan = plan(&bundle, &p, &ImportOptions::default(), &live()).expect("plan"); let token_line = find(&plan.skipped, "auth.user_auth_token").expect("auth token skipped"); assert!(token_line.why.contains("--include-auth"), "{}", token_line.why); assert!(plan.auth_token.is_none(), "token must not be queued without the gate"); let secret = find(&plan.skipped, "integrations.scrobblers.lastfm_session_key") .expect("scrobbler secret skipped"); assert!(secret.why.contains("--include-auth"), "{}", secret.why); cleanup(&p); } #[test] fn secret_applies_with_gate() { // The other half of the double gate: with --include-auth the token is queued // for validation and the scrobbler secret applies. let p = scratch("secret-gate"); std::fs::create_dir_all(&p.data_root).unwrap(); write_last_user_id(&p.data_root, 1234567).unwrap(); let bundle = bundle_with(json!({ "integrations": { "scrobblers": { "lastfm_session_key": "d580secret" } }, "auth": { "user_auth_token": "Bo4Asecret", "user_id": 1234567 } })); let opts = ImportOptions { include_auth: true, ..Default::default() }; let plan = plan(&bundle, &p, &opts, &live()).expect("plan"); assert_eq!(plan.auth_token.as_deref(), Some("Bo4Asecret")); assert_eq!(plan.bundle_user_id, Some(1234567)); assert!(find(&plan.applied, "integrations.scrobblers.lastfm_session_key").is_some()); cleanup(&p); } #[test] fn version_gate_rejects_newer() { // §5.6: a bundle newer than this importer is rejected. let p = scratch("version"); let mut bundle = bundle_with(json!({ "audio": { "gapless_enabled": true } })); bundle.schema_version = 2; let err = plan(&bundle, &p, &ImportOptions::default(), &live()).unwrap_err(); match err { BundleError::VersionTooNew { bundle, supported } => { assert_eq!(bundle, 2); assert_eq!(supported, 1); } other => panic!("expected VersionTooNew, got {other:?}"), } cleanup(&p); } #[test] fn missing_device_non_tty_falls_back_safe() { // §5.3 step 4 non-TTY: backend→SystemDefault, device→null, intent flags→false, // all reported in adapted; no device_pick and never hangs. let p = scratch("nontty"); let bundle = bundle_with(json!({ "audio": { "output_device": "hw:9,9", "backend_type": "Jack", "exclusive_mode": true, "dac_passthrough": true } })); let opts = ImportOptions { non_tty: true, ..Default::default() }; let plan = plan(&bundle, &p, &opts, &live()).expect("plan"); assert!(plan.device_pick.is_none(), "non-tty must not request a pick"); let dev = find(&plan.adapted, "audio.output_device").expect("device adapted"); assert_eq!(dev.old.as_deref(), Some("hw:9,9")); assert_eq!(write_of(&plan, "audio.output_device"), Some(&Value::Null)); let backend = find(&plan.adapted, "audio.backend_type").expect("backend adapted"); assert_eq!(write_of(&plan, "audio.backend_type"), Some(&json!("SystemDefault"))); assert!(backend.new.contains("SystemDefault")); for flag in ["audio.exclusive_mode", "audio.dac_passthrough"] { let l = find(&plan.adapted, flag).unwrap_or_else(|| panic!("{flag} must adapt")); assert_eq!(l.new, "false", "{flag} must reset to false"); assert_eq!(write_of(&plan, flag), Some(&Value::Bool(false))); } cleanup(&p); } #[test] fn found_device_applies_verbatim() { // A machine field that validates cleanly lands in APPLIED (§5.4). let p = scratch("found"); let bundle = bundle_with(json!({ "audio": { "output_device": "hw:1,0", "backend_type": "Alsa", "exclusive_mode": true } })); let plan = plan(&bundle, &p, &ImportOptions::default(), &live()).expect("plan"); assert!(plan.device_pick.is_none()); assert_eq!(find(&plan.applied, "audio.output_device").unwrap().new, "hw:1,0"); assert_eq!(find(&plan.applied, "audio.backend_type").unwrap().new, "Alsa"); assert_eq!(find(&plan.applied, "audio.exclusive_mode").unwrap().new, "true"); assert!(plan.adapted.is_empty(), "clean validation must not adapt: {:?}", plan.adapted); cleanup(&p); } #[test] fn absent_fields_leave_target_untouched() { // §7: only present fields have effects. let p = scratch("absent"); let bundle = bundle_with(json!({ "playback": { "autoplay_mode": "track_only" } })); let plan = plan(&bundle, &p, &ImportOptions::default(), &live()).expect("plan"); // Exactly one write (the single present field); nothing audio/qconnect/etc. assert_eq!(plan.writes.len(), 1); assert_eq!(plan.writes[0].0, "playback.autoplay_mode"); assert!(plan.writes.iter().all(|(k, _)| !k.starts_with("audio."))); cleanup(&p); } #[test] fn machine_caches_always_skipped() { // §2.2/§3: source-machine device caches are meaningless on the target. let p = scratch("caches"); let bundle = bundle_with(json!({ "audio": { "device_max_sample_rate": 768000, "device_sample_rate_limits": { "hw:4,0": 768000 } } })); let plan = plan(&bundle, &p, &ImportOptions::default(), &live()).expect("plan"); for key in ["audio.device_max_sample_rate", "audio.device_sample_rate_limits"] { let l = find(&plan.skipped, key).unwrap_or_else(|| panic!("{key} must skip")); assert!(l.why.contains("device cache"), "{}", l.why); } assert!(plan .writes .iter() .all(|(k, _)| !k.contains("device_max_sample_rate") && !k.contains("device_sample_rate_limits"))); cleanup(&p); } #[test] fn roundtrip_same_box_is_noop() { // §7 acceptance invariant: export(daemon) → plan(same box) ⇒ adapted EMPTY, // applied values == current values, skipped == the always-skip caches only. let p = scratch("roundtrip"); std::fs::create_dir_all(&p.data_root).unwrap(); // Configure a realistic daemon (never "ask", never "remember_last", a real // device that this box's LiveSystem enumerates). { let audio = AudioSettingsStore::new_at(&p.data_root).unwrap(); audio.set_backend_type(Some(AudioBackendType::Alsa)).unwrap(); audio.set_output_device(Some("hw:1,0")).unwrap(); audio.set_exclusive_mode(true).unwrap(); audio.set_dsd_mode("dop").unwrap(); // a working DSD daemon audio.set_quality_fallback_behavior("always_fallback").unwrap(); audio.set_gapless_enabled(true).unwrap(); let pb = PlaybackPreferencesStore::new_at(&p.data_root).unwrap(); pb.set_persist_session(true).unwrap(); qconnect_kv_write( &p.data_root.join("qconnect_settings.db"), "device_name", Some("Estudio"), ) .unwrap(); qconnect_kv_write( &p.data_root.join("qconnect_settings.db"), "startup_mode", Some("on"), ) .unwrap(); } let src = ExportSource::Daemon(ProfilePaths { config_root: p.config_root.clone(), data_root: p.data_root.clone(), }); let bundle = export(src, &ExportOptions::default()).expect("export"); let plan = plan(&bundle, &p, &ImportOptions::default(), &live()).expect("plan"); assert!( plan.adapted.is_empty(), "roundtrip must not adapt anything: {:?}", plan.adapted ); assert!(plan.device_pick.is_none()); // dsd dop survived without --trust-dsd (no-change short-circuit). assert_eq!(find(&plan.applied, "audio.dsd_mode").unwrap().new, "dop"); assert_eq!(find(&plan.applied, "audio.output_device").unwrap().new, "hw:1,0"); assert_eq!(find(&plan.applied, "qconnect.startup_mode").unwrap().new, "on"); // Every skipped line must be one of the always-skip caches. for l in &plan.skipped { assert!( l.key.contains("device_max_sample_rate") || l.key.contains("device_sample_rate_limits"), "unexpected skip in roundtrip: {} ({})", l.key, l.why ); } cleanup(&p); } #[test] fn remember_last_maps_to_on_in_adapted() { // §5.5: startup_mode remember_last → on (daemon has no last-state tracking). let p = scratch("remember"); let bundle = bundle_with(json!({ "qconnect": { "startup_mode": "remember_last" } })); let plan = plan(&bundle, &p, &ImportOptions::default(), &live()).expect("plan"); let line = find(&plan.adapted, "qconnect.startup_mode").expect("adapted"); assert_eq!(line.old.as_deref(), Some("remember_last")); assert_eq!(line.new, "on"); cleanup(&p); } #[test] fn library_folders_skipped_on_daemon() { // §2.6: the P0 daemon has no local library. let p = scratch("folders"); let bundle = bundle_with(json!({ "library_folders": [ { "path": "/mnt/music", "network_fs": false } ] })); let plan = plan(&bundle, &p, &ImportOptions::default(), &live()).expect("plan"); let line = find_contains(&plan.skipped, "library_folders").expect("skipped"); assert!(line.why.contains("no local library"), "{}", line.why); cleanup(&p); } #[test] fn apply_writes_are_idempotent_and_persist() { // §5.3 step 6: pure setter writes; a second apply is safe and lands the same // values. Exercises the whole applied/adapted write path end-to-end. let p = scratch("apply"); let bundle = bundle_with(json!({ "playback": { "autoplay_mode": "track_only", "persist_session": false }, "audio": { "output_device": "hw:1,0", "backend_type": "Alsa", "gapless_enabled": true, "dsd_mode": "dop" }, "prefs": { "streaming_quality": "cd" }, "qconnect": { "device_name": "Kitchen", "startup_mode": "on" } })); let opts = ImportOptions { trust_dsd: true, ..Default::default() }; let plan = plan(&bundle, &p, &opts, &live()).expect("plan"); apply(&plan, &p, None).expect("apply once"); apply(&plan, &p, None).expect("apply twice (idempotent)"); let audio = AudioSettingsStore::new_at(&p.data_root).unwrap().get_settings().unwrap(); assert_eq!(audio.output_device.as_deref(), Some("hw:1,0")); assert_eq!(audio.backend_type, Some(AudioBackendType::Alsa)); assert!(audio.gapless_enabled); assert_eq!(audio.dsd_mode, "dop"); let pb = PlaybackPreferencesStore::new_at(&p.data_root).unwrap().get_preferences().unwrap(); assert!(!pb.persist_session); assert_eq!(daemon_prefs::load_at(&p.data_root).streaming_quality, "cd"); assert_eq!( qconnect_kv_read(&p.data_root.join("qconnect_settings.db"), "device_name").as_deref(), Some("Kitchen") ); cleanup(&p); } #[test] fn bundle_json_roundtrips_flat() { // The on-disk shape is flat (§2.9): header + domains at the top level. let bundle = bundle_with(json!({ "audio": { "gapless_enabled": true } })); let text = bundle.to_json_string().unwrap(); let v: serde_json::Value = serde_json::from_str(&text).unwrap(); assert_eq!(v["schema_version"], 1); assert_eq!(v["source"]["profile"], "desktop"); assert_eq!(v["audio"]["gapless_enabled"], true); let reparsed = Bundle::parse(&text).unwrap(); assert_eq!(reparsed.schema_version, 1); assert!(reparsed.domains.contains_key("audio")); } #[test] fn parse_rejects_missing_version() { let err = Bundle::parse(r#"{ "audio": {} }"#).unwrap_err(); assert!(matches!(err, BundleError::VersionMalformed)); } #[test] fn secret_values_never_render_in_summary() { // §5.4: SECRET-class values are MASKED in the summary lines — the raw value // rides only the write list (terminal scrollback / CI logs must never see it). let p = scratch("mask"); std::fs::create_dir_all(&p.data_root).unwrap(); write_last_user_id(&p.data_root, 1).unwrap(); let bundle = bundle_with(json!({ "integrations": { "scrobblers": { "lastfm_session_key": "d580REALSECRET", "listenbrainz_token": "" } } })); let opts = ImportOptions { include_auth: true, ..Default::default() }; let plan = plan(&bundle, &p, &opts, &live()).expect("plan"); let key_line = find(&plan.applied, "integrations.scrobblers.lastfm_session_key").unwrap(); assert_eq!(key_line.new, "(secret, applied)"); let empty_line = find(&plan.applied, "integrations.scrobblers.listenbrainz_token").unwrap(); assert_eq!(empty_line.new, "(empty)"); // No rendered bucket line anywhere carries the raw secret. let rendered = format!("{:?} {:?} {:?}", plan.applied, plan.adapted, plan.skipped); assert!(!rendered.contains("d580REALSECRET"), "secret leaked into summary: {rendered}"); // The write list still carries the real value so apply works. assert_eq!( write_of(&plan, "integrations.scrobblers.lastfm_session_key"), Some(&json!("d580REALSECRET")) ); cleanup(&p); } #[test] fn contains_secrets_keys_on_actual_secret_values() { // §3 warning trigger: any secret VALUE present (auth token OR a non-blank // scrobbler secret) — not the auth domain alone. let none = bundle_with(json!({ "audio": { "gapless_enabled": true } })); assert!(!none.contains_secrets()); let blank = bundle_with(json!({ "integrations": { "scrobblers": { "lastfm_session_key": "", "listenbrainz_token": "" } } })); assert!(!blank.contains_secrets(), "blank secrets are not secrets"); let scrob = bundle_with(json!({ "integrations": { "scrobblers": { "lastfm_session_key": "sk-live" } } })); assert!(scrob.contains_secrets()); let auth = bundle_with(json!({ "auth": { "user_auth_token": "tok" } })); assert!(auth.contains_secrets()); } #[test] fn device_pick_names_the_backend() { // §5.4 prompt: "Available on Alsa:" — the pick carries the backend name. let p = scratch("pick-backend"); let bundle = bundle_with(json!({ "audio": { "output_device": "hw:9,9", "backend_type": "Alsa" } })); let plan = plan(&bundle, &p, &ImportOptions::default(), &live()).expect("plan"); let pick = plan.device_pick.expect("TTY plan must request a pick"); assert_eq!(pick.backend, "Alsa"); assert_eq!(pick.wanted, "hw:9,9"); cleanup(&p); }