From 1bb778db9525cfaab41200b1f136c88252f523fe Mon Sep 17 00:00:00 2001 From: Markus Kovero Date: Thu, 3 Sep 2026 04:27:55 +0000 Subject: [PATCH 1/2] fix: propagate corrupt calibration errors --- ac-rs/ZMQ.md | 6 + .../ac-core/src/shared/calibration/store.rs | 15 +- .../src/handlers/audio/monitor/channel.rs | 3 +- .../src/handlers/audio/monitor/mod.rs | 19 ++- .../ac-daemon/src/handlers/audio/plot.rs | 8 +- ac-rs/crates/ac-daemon/src/handlers/mod.rs | 44 +++++- .../crates/ac-daemon/src/handlers/test_dut.rs | 4 +- .../crates/ac-daemon/src/handlers/test_hw.rs | 14 +- .../ac-daemon/src/handlers/transfer/plan.rs | 10 +- .../tests/it_protocol/corrupt_cal.rs | 134 ++++++++++++++++++ .../ac-daemon/tests/it_protocol/main.rs | 1 + 11 files changed, 226 insertions(+), 32 deletions(-) create mode 100644 ac-rs/crates/ac-daemon/tests/it_protocol/corrupt_cal.rs diff --git a/ac-rs/ZMQ.md b/ac-rs/ZMQ.md index 778344b2..adfa8c42 100644 --- a/ac-rs/ZMQ.md +++ b/ac-rs/ZMQ.md @@ -47,6 +47,12 @@ Every CTRL reply contains at minimum: On failure: `"ok": false, "error": ""`. +Commands that use calibration refuse synchronously when `cal.json` exists +but cannot be read or parsed. No worker starts and no measurement frames are +published. The error string names the store and cause and confirms that the +existing file was preserved; transfer refusals also state that the failure +applies to all requested pairs. + --- ## DATA frame envelope diff --git a/ac-rs/crates/ac-core/src/shared/calibration/store.rs b/ac-rs/crates/ac-core/src/shared/calibration/store.rs index 017e7aad..076d961e 100644 --- a/ac-rs/crates/ac-core/src/shared/calibration/store.rs +++ b/ac-rs/crates/ac-core/src/shared/calibration/store.rs @@ -50,13 +50,7 @@ fn read_all_entries(path: &Path) -> Result> { } let raw = std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?; - serde_json::from_str(&raw).with_context(|| { - format!( - "parsing {} — refusing to treat it as empty, because saving over it would \ - discard every calibration it holds", - path.display() - ) - }) + serde_json::from_str(&raw).with_context(|| format!("parsing {}", path.display())) } /// Serialize `all` to `path` atomically: write a sibling temporary, then @@ -90,7 +84,12 @@ impl Calibration { /// Existing entries for other channel pairs are preserved. pub fn save(&self, path: Option<&Path>) -> Result<()> { let path = resolve_path(path); - let mut all = read_all_entries(&path)?; + let mut all = read_all_entries(&path).with_context(|| { + format!( + "refusing to save over unreadable calibration store {} — existing file preserved", + path.display() + ) + })?; all.insert(self.key(), self.to_entry()); write_all_entries(&path, &all)?; eprintln!( diff --git a/ac-rs/crates/ac-daemon/src/handlers/audio/monitor/channel.rs b/ac-rs/crates/ac-daemon/src/handlers/audio/monitor/channel.rs index cddfaa8b..2efdd4b9 100644 --- a/ac-rs/crates/ac-daemon/src/handlers/audio/monitor/channel.rs +++ b/ac-rs/crates/ac-daemon/src/handlers/audio/monitor/channel.rs @@ -368,12 +368,11 @@ impl ChannelState { pub(super) fn new( channel: u32, in_port: String, - out_ch: u32, + cal: Option, sr: u32, freq_hz: f64, caps: &RingCaps, ) -> Self { - let cal = Calibration::load(out_ch, channel, None).ok().flatten(); let spl_offset = cal.as_ref().and_then(Calibration::spl_offset_db); let mic_curve = cal.as_ref().and_then(|c| c.mic_response.clone()); let loudness_fir = mic_curve diff --git a/ac-rs/crates/ac-daemon/src/handlers/audio/monitor/mod.rs b/ac-rs/crates/ac-daemon/src/handlers/audio/monitor/mod.rs index a539adc6..c0158490 100644 --- a/ac-rs/crates/ac-daemon/src/handlers/audio/monitor/mod.rs +++ b/ac-rs/crates/ac-daemon/src/handlers/audio/monitor/mod.rs @@ -29,7 +29,9 @@ use ac_core::visualize::weighting_curves::WeightingCurve; use crate::audio::make_engine; use crate::server::{MonitorParams, ServerState}; -use super::super::{busy_guard, cfg_guard, resolve_input, send_pub, spawn_worker}; +use super::super::{ + busy_guard, cfg_guard, load_calibration_or_refuse, resolve_input, send_pub, spawn_worker, +}; use self::capture::{ capture_budget_samples, capture_into_ring, capture_or_report, log_transform_time, @@ -143,10 +145,18 @@ pub fn monitor_spectrum(state: &ServerState, cmd: &Value) -> Value { Err(e) => return json!({"ok": false, "error": e}), }; let primary_in_port = in_ports.first().cloned().unwrap_or_default(); + let out_ch = cfg.output_channel; + + let mut channel_cals = Vec::with_capacity(channels.len()); + for &channel in &channels { + match load_calibration_or_refuse(out_ch, channel, "measurement", None) { + Ok(cal) => channel_cals.push(cal), + Err(msg) => return json!({"ok": false, "error": msg}), + } + } let pub_tx = state.pub_tx.clone(); let fake = state.fake_audio; - let out_ch = cfg.output_channel; let n_channels = channels.len() as u32; let channels_worker = channels.clone(); let in_ports_worker = in_ports.clone(); @@ -280,8 +290,9 @@ pub fn monitor_spectrum(state: &ServerState, cmd: &Value) -> Value { let mut channel_states: Vec = channels_worker .iter() .zip(in_ports_worker.iter()) - .map(|(&channel, in_port)| { - ChannelState::new(channel, in_port.clone(), out_ch, sr, freq_hz, &ring_caps) + .zip(channel_cals) + .map(|((&channel, in_port), cal)| { + ChannelState::new(channel, in_port.clone(), cal, sr, freq_hz, &ring_caps) }) .collect(); let single_channel = channel_states.len() == 1; diff --git a/ac-rs/crates/ac-daemon/src/handlers/audio/plot.rs b/ac-rs/crates/ac-daemon/src/handlers/audio/plot.rs index dd9f71fb..40a60f24 100644 --- a/ac-rs/crates/ac-daemon/src/handlers/audio/plot.rs +++ b/ac-rs/crates/ac-daemon/src/handlers/audio/plot.rs @@ -26,7 +26,7 @@ use crate::audio::make_engine; use crate::server::ServerState; use super::super::{ - apply_drive_ceiling, busy_guard, cfg_guard, resolve_input, resolve_output, send_pub, + apply_drive_ceiling, busy_guard, cal_guard, cfg_guard, resolve_input, resolve_output, send_pub, snapshot_from_cal, spawn_worker, sweep_point_frame, Tier1Ctx, }; use crate::handlers::mic; @@ -67,6 +67,7 @@ pub fn plot(state: &ServerState, cmd: &Value) -> Value { let fake = state.fake_audio; let out_ch = cfg.output_channel; let in_ch = cfg.input_channel; + let cal = cal_guard!(out_ch, in_ch); // Processing-context shared state — same Arc clones the monitor // worker uses so #97 + #98 wire the same envelope onto Tier 1. let mic_corr_enabled = state.mic_correction_enabled.clone(); @@ -81,7 +82,6 @@ pub fn plot(state: &ServerState, cmd: &Value) -> Value { let temperature_c = cfg.temperature_c; let worker = spawn_worker(state, "plot", move |stop| { - let cal = Calibration::load(out_ch, in_ch, None).ok().flatten(); let mic_curve_opt = cal.as_ref().and_then(|c| c.mic_response.clone()); let spl_offset = cal.as_ref().and_then(Calibration::spl_offset_db); let freqs = super::super::log_freq_points(start_hz, stop_hz, ppd); @@ -328,12 +328,12 @@ pub fn plot_level(state: &ServerState, cmd: &Value) -> Value { let fake = state.fake_audio; let out_ch = cfg.output_channel; let in_ch = cfg.input_channel; + let cal = cal_guard!(out_ch, in_ch); let mic_corr_enabled = state.mic_correction_enabled.clone(); let band_weighting_shared = state.band_weighting.clone(); let time_integration_shared = state.time_integration_mode.clone(); let worker = spawn_worker(state, "plot_level", move |stop| { - let cal = Calibration::load(out_ch, in_ch, None).ok().flatten(); let mic_curve_opt = cal.as_ref().and_then(|c| c.mic_response.clone()); let spl_offset = cal.as_ref().and_then(Calibration::spl_offset_db); // Raw request shape — each computed level is clamped individually @@ -636,6 +636,7 @@ pub fn plot_ir(state: &ServerState, cmd: &Value) -> Value { let out_port_reply = out_port.clone(); let out_ch = cfg.output_channel; let in_ch = cfg.input_channel; + let cal = cal_guard!(out_ch, in_ch); let report_dir = cfg.report_dir.clone(); let temperature_c = cfg.temperature_c; let device = cfg.device; @@ -655,7 +656,6 @@ pub fn plot_ir(state: &ServerState, cmd: &Value) -> Value { // `gated_points` correction step below for why: `MicCurveFir`'s // linear-phase group delay would otherwise move the IR peak the // gate is anchored to. - let cal = Calibration::load(out_ch, in_ch, None).ok().flatten(); let mic_curve_opt = cal.as_ref().and_then(|c| c.mic_response.clone()); let mut eng = make_engine(fake); diff --git a/ac-rs/crates/ac-daemon/src/handlers/mod.rs b/ac-rs/crates/ac-daemon/src/handlers/mod.rs index 0865c229..c838feec 100644 --- a/ac-rs/crates/ac-daemon/src/handlers/mod.rs +++ b/ac-rs/crates/ac-daemon/src/handlers/mod.rs @@ -18,7 +18,7 @@ use std::thread; use serde_json::{json, Value}; use ac_core::config::Config; -use ac_core::shared::calibration::Calibration; +use ac_core::shared::calibration::{default_cal_path, Calibration}; use crate::audio::{make_engine, AudioEngine}; use crate::server::ServerState; @@ -115,6 +115,48 @@ macro_rules! cfg_guard { } pub(super) use cfg_guard; +// --------------------------------------------------------------------------- +// Calibration-freshness guard (#425) +// --------------------------------------------------------------------------- + +/// Load one calibration entry, refusing the operation if the shared store +/// exists but cannot be read or parsed. A read error is not "no calibration": +/// continuing would silently relabel an uncalibrated result as successful. +pub(super) fn load_calibration_or_refuse( + output_channel: u32, + input_channel: u32, + operation: &str, + scope: Option<&str>, +) -> Result, String> { + Calibration::load(output_channel, input_channel, None).map_err(|e| { + let scope = scope + .map(|value| format!("\n scope {value}")) + .unwrap_or_default(); + format!( + "calibration unreadable — {operation} not started{scope}\n\ + \x20 store {}\n\ + \x20 cause {e:#}\n\ + \x20 data existing file preserved", + default_cal_path().display() + ) + }) +} + +macro_rules! cal_guard { + ($output_channel:expr, $input_channel:expr) => { + match $crate::handlers::load_calibration_or_refuse( + $output_channel, + $input_channel, + "measurement", + None, + ) { + Ok(cal) => cal, + Err(msg) => return ::serde_json::json!({"ok": false, "error": msg}), + } + }; +} +pub(super) use cal_guard; + // --------------------------------------------------------------------------- // Worker spawn // --------------------------------------------------------------------------- diff --git a/ac-rs/crates/ac-daemon/src/handlers/test_dut.rs b/ac-rs/crates/ac-daemon/src/handlers/test_dut.rs index d662f176..1df501c4 100644 --- a/ac-rs/crates/ac-daemon/src/handlers/test_dut.rs +++ b/ac-rs/crates/ac-daemon/src/handlers/test_dut.rs @@ -13,7 +13,7 @@ use crate::handlers::mic; use crate::server::ServerState; use super::{ - busy_guard, cal_dbu_str, cal_out_dbu_str, capture_rms, cfg_guard, median, + busy_guard, cal_dbu_str, cal_guard, cal_out_dbu_str, capture_rms, cfg_guard, median, ref_output_migration_warning, resolve_input, resolve_output, resolve_ref_input, resolve_ref_output, rms_to_dbfs, send_pub, spawn_worker, TestResult, }; @@ -55,6 +55,7 @@ pub fn test_dut(state: &ServerState, cmd: &Value) -> Value { }; let out_ch = cfg.output_channel; let in_ch = cfg.input_channel; + let cal = cal_guard!(out_ch, in_ch); let pub_tx = state.pub_tx.clone(); let fake = state.fake_audio; @@ -98,7 +99,6 @@ pub fn test_dut(state: &ServerState, cmd: &Value) -> Value { } let sr = eng.sample_rate(); - let cal = Calibration::load(out_ch, in_ch, None).ok().flatten(); let mic_curve_loaded = cal .as_ref() .map(|c| c.mic_response.is_some()) diff --git a/ac-rs/crates/ac-daemon/src/handlers/test_hw.rs b/ac-rs/crates/ac-daemon/src/handlers/test_hw.rs index cc12ac59..b6fb9fa5 100644 --- a/ac-rs/crates/ac-daemon/src/handlers/test_hw.rs +++ b/ac-rs/crates/ac-daemon/src/handlers/test_hw.rs @@ -13,9 +13,9 @@ use crate::handlers::mic; use crate::server::ServerState; use super::{ - analyze_mono, busy_guard, capture_rms, cfg_guard, read_dmm_vrms, ref_output_migration_warning, - resolve_input, resolve_output, resolve_ref_input, resolve_ref_output, rms_to_dbfs, send_pub, - spawn_worker, std_dev, TestResult, + analyze_mono, busy_guard, cal_guard, capture_rms, cfg_guard, read_dmm_vrms, + ref_output_migration_warning, resolve_input, resolve_output, resolve_ref_input, + resolve_ref_output, rms_to_dbfs, send_pub, spawn_worker, std_dev, TestResult, }; pub fn test_hardware(state: &ServerState, cmd: &Value) -> Value { @@ -55,6 +55,7 @@ pub fn test_hardware(state: &ServerState, cmd: &Value) -> Value { let dmm_host = cfg.dmm_host.clone(); let out_ch = cfg.output_channel; let in_ch = cfg.input_channel; + let cal_ctx = cal_guard!(out_ch, in_ch); let mic_corr_enabled = state.mic_correction_enabled.clone(); let out_port_r = out_port.clone(); @@ -98,7 +99,6 @@ pub fn test_hardware(state: &ServerState, cmd: &Value) -> Value { // once per worker, stamped on every emitted `test_result` so // downstream readers can tell whether the test ran on a // mic-curve'd channel and at what SPL offset (#103). - let cal_ctx = Calibration::load(out_ch, in_ch, None).ok().flatten(); let mic_curve_loaded = cal_ctx .as_ref() .map(|c| c.mic_response.is_some()) @@ -147,8 +147,6 @@ pub fn test_hardware(state: &ServerState, cmd: &Value) -> Value { let mut dmm_pass = 0usize; if dmm_mode { if let Some(ref host) = dmm_host { - let cal = Calibration::load(out_ch, in_ch, None).ok().flatten(); - macro_rules! emit_dmm { ($r:expr) => {{ if $r.pass { dmm_pass += 1; } @@ -162,10 +160,10 @@ pub fn test_hardware(state: &ServerState, cmd: &Value) -> Value { } if !stop.load(Ordering::Relaxed) { - emit_dmm!(hw_dmm_absolute(&mut *eng, host, cal.as_ref())); + emit_dmm!(hw_dmm_absolute(&mut *eng, host, cal_ctx.as_ref())); } if !stop.load(Ordering::Relaxed) { - emit_dmm!(hw_dmm_tracking(&mut *eng, host, cal.as_ref())); + emit_dmm!(hw_dmm_tracking(&mut *eng, host, cal_ctx.as_ref())); } if !stop.load(Ordering::Relaxed) { emit_dmm!(hw_dmm_freq_response(&mut *eng, host)); diff --git a/ac-rs/crates/ac-daemon/src/handlers/transfer/plan.rs b/ac-rs/crates/ac-daemon/src/handlers/transfer/plan.rs index b1a9f8d0..6c601768 100644 --- a/ac-rs/crates/ac-daemon/src/handlers/transfer/plan.rs +++ b/ac-rs/crates/ac-daemon/src/handlers/transfer/plan.rs @@ -22,7 +22,8 @@ use ac_core::visualize::weighting_curves::WeightingCurve; use crate::audio::make_engine; use crate::handlers::{ - apply_drive_ceiling, ref_output_migration_warning, resolve_output, resolve_ref_output, + apply_drive_ceiling, load_calibration_or_refuse, ref_output_migration_warning, resolve_output, + resolve_ref_output, }; use crate::server::ServerState; @@ -159,8 +160,11 @@ impl SessionPlan { // the exact split this check exists to prevent. let unique_cals: Vec> = unique_chans .iter() - .map(|&ch| Calibration::load(out_ch, ch, None).ok().flatten()) - .collect(); + .map(|&ch| { + load_calibration_or_refuse(out_ch, ch, "transfer", Some("all requested pairs")) + }) + .collect::>() + .map_err(|msg| json!({"ok": false, "error": msg}))?; // Every channel named in `pairs` is in `unique_chans` by // construction, so this never misses: `None` means "no calibration // stored for this channel", never "channel not found". diff --git a/ac-rs/crates/ac-daemon/tests/it_protocol/corrupt_cal.rs b/ac-rs/crates/ac-daemon/tests/it_protocol/corrupt_cal.rs new file mode 100644 index 00000000..f9172550 --- /dev/null +++ b/ac-rs/crates/ac-daemon/tests/it_protocol/corrupt_cal.rs @@ -0,0 +1,134 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use serde_json::{json, Value}; + +use super::common::{Client, Daemon}; + +const CORRUPT_CAL: &[u8] = br#"{"out0_in0": not-json}"#; +const UNREADABLE_CAL: &[u8] = &[0xff]; + +fn cal_path(d: &Daemon) -> PathBuf { + d.home.join(".config").join("ac").join("cal.json") +} + +fn write_corrupt_cal(d: &Daemon) -> PathBuf { + let path = cal_path(d); + fs::write(&path, CORRUPT_CAL).expect("write corrupt calibration store"); + path +} + +fn assert_refused(reply: &Value, path: &Path, operation: &str, scope: Option<&str>) { + assert_eq!(reply["ok"], false, "operation was not refused: {reply}"); + let error = reply["error"] + .as_str() + .unwrap_or_else(|| panic!("refusal error was not a string: {reply}")); + assert!( + error.starts_with(&format!("calibration unreadable — {operation} not started")), + "wrong refusal: {error}" + ); + if let Some(scope) = scope { + assert!(error.contains(&format!("\n scope {scope}"))); + } + assert!(error.contains(&format!("\n store {}", path.display()))); + assert!(error.contains("\n cause parsing ")); + assert!(error.contains("\n data existing file preserved")); +} + +fn assert_store_preserved(path: &Path) { + assert_eq!( + fs::read(path).expect("read calibration store after refusal"), + CORRUPT_CAL + ); +} + +fn configure_reference(client: &Client<'_>) { + let reply = client.call(json!({ + "cmd": "setup", + "update": {"reference_channel": 1} + })); + assert_eq!(reply["ok"], true, "reference setup failed: {reply}"); +} + +#[test] +fn corrupt_cal_refuses_every_plot_read_and_preserves_store() { + let d = Daemon::spawn(); + let client = Client::new(&d); + let path = write_corrupt_cal(&d); + + for cmd in ["plot", "plot_level", "plot_ir"] { + let reply = client.call(json!({"cmd": cmd})); + assert_refused(&reply, &path, "measurement", None); + } + assert_store_preserved(&path); +} + +#[test] +fn unreadable_cal_refuses_measurement_and_preserves_store() { + let d = Daemon::spawn(); + let client = Client::new(&d); + let path = cal_path(&d); + fs::write(&path, UNREADABLE_CAL).expect("write non-UTF-8 calibration store"); + + let reply = client.call(json!({"cmd": "plot"})); + assert_eq!(reply["ok"], false, "operation was not refused: {reply}"); + let error = reply["error"] + .as_str() + .unwrap_or_else(|| panic!("refusal error was not a string: {reply}")); + assert!(error.contains(&format!("\n store {}", path.display()))); + assert!(error.contains("\n cause reading ")); + assert!(error.contains("\n data existing file preserved")); + assert_eq!( + fs::read(&path).expect("read calibration store after refusal"), + UNREADABLE_CAL + ); +} + +#[test] +fn corrupt_cal_refuses_monitor_and_preserves_store() { + let d = Daemon::spawn(); + let client = Client::new(&d); + let path = write_corrupt_cal(&d); + + let reply = client.call(json!({"cmd": "monitor_spectrum", "channels": [0, 1]})); + assert_refused(&reply, &path, "measurement", None); + assert_store_preserved(&path); +} + +#[test] +fn corrupt_cal_refuses_transfer_and_preserves_store() { + let d = Daemon::spawn(); + let client = Client::new(&d); + let path = write_corrupt_cal(&d); + + let reply = client.call(json!({ + "cmd": "transfer_stream", + "pairs": [[0, 1]] + })); + assert_refused(&reply, &path, "transfer", Some("all requested pairs")); + assert_store_preserved(&path); +} + +#[test] +fn corrupt_cal_refuses_dut_test_and_preserves_store() { + let d = Daemon::spawn(); + let client = Client::new(&d); + configure_reference(&client); + let path = write_corrupt_cal(&d); + + let reply = client.call(json!({"cmd": "test_dut"})); + assert_refused(&reply, &path, "measurement", None); + assert_store_preserved(&path); +} + +#[test] +fn corrupt_cal_refuses_hardware_test_and_preserves_store() { + let d = Daemon::spawn(); + let client = Client::new(&d); + configure_reference(&client); + let path = write_corrupt_cal(&d); + + let reply = client.call(json!({"cmd": "test_hardware"})); + assert_refused(&reply, &path, "measurement", None); + assert_store_preserved(&path); +} diff --git a/ac-rs/crates/ac-daemon/tests/it_protocol/main.rs b/ac-rs/crates/ac-daemon/tests/it_protocol/main.rs index 8776a4af..571c2877 100644 --- a/ac-rs/crates/ac-daemon/tests/it_protocol/main.rs +++ b/ac-rs/crates/ac-daemon/tests/it_protocol/main.rs @@ -14,6 +14,7 @@ mod common; mod basics; mod calibrate; +mod corrupt_cal; mod level_clamp; mod modes; mod monitor; From a611241d4fead17dbec5347770c58dce75508a73 Mon Sep 17 00:00:00 2001 From: Markus Kovero Date: Thu, 3 Sep 2026 04:42:16 +0000 Subject: [PATCH 2/2] fix: keep refused monitor inactive --- .../src/handlers/audio/monitor/mod.rs | 24 +++++++++---------- .../tests/it_protocol/corrupt_cal.rs | 12 ++++++++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/ac-rs/crates/ac-daemon/src/handlers/audio/monitor/mod.rs b/ac-rs/crates/ac-daemon/src/handlers/audio/monitor/mod.rs index c0158490..d3292748 100644 --- a/ac-rs/crates/ac-daemon/src/handlers/audio/monitor/mod.rs +++ b/ac-rs/crates/ac-daemon/src/handlers/audio/monitor/mod.rs @@ -103,18 +103,6 @@ pub fn monitor_spectrum(state: &ServerState, cmd: &Value) -> Value { let lf_fft_n = defaults.lf_fft_n; let crossover_hz = defaults.crossover_hz; - { - let mut mp = state.monitor_params.lock().unwrap(); - *mp = MonitorParams { - interval, - fft_n, - lf_fft_n, - crossover_hz, - active: true, - }; - } - let monitor_params_shared = state.monitor_params.clone(); - let cfg = state.cfg.lock().unwrap().clone(); let channels: Vec = cmd @@ -170,6 +158,18 @@ pub fn monitor_spectrum(state: &ServerState, cmd: &Value) -> Value { let loudness_reset_shared = state.loudness_reset_request.clone(); let band_weighting_shared = state.band_weighting.clone(); + { + let mut mp = state.monitor_params.lock().unwrap(); + *mp = MonitorParams { + interval, + fft_n, + lf_fft_n, + crossover_hz, + active: true, + }; + } + let monitor_params_shared = state.monitor_params.clone(); + let worker = spawn_worker(state, "monitor_spectrum", move |stop| { let mut eng = make_engine(fake); let start_port = in_ports_worker.first().map(String::as_str); diff --git a/ac-rs/crates/ac-daemon/tests/it_protocol/corrupt_cal.rs b/ac-rs/crates/ac-daemon/tests/it_protocol/corrupt_cal.rs index f9172550..ac11ebdc 100644 --- a/ac-rs/crates/ac-daemon/tests/it_protocol/corrupt_cal.rs +++ b/ac-rs/crates/ac-daemon/tests/it_protocol/corrupt_cal.rs @@ -92,6 +92,18 @@ fn corrupt_cal_refuses_monitor_and_preserves_store() { let reply = client.call(json!({"cmd": "monitor_spectrum", "channels": [0, 1]})); assert_refused(&reply, &path, "measurement", None); + + let params_reply = client.call(json!({ + "cmd": "set_monitor_params", + "interval": 0.1, + "fft_n": 4096 + })); + assert_eq!( + params_reply["ok"], false, + "stale monitor state: {params_reply}" + ); + assert_eq!(params_reply["error"], "no active monitor"); + assert_store_preserved(&path); }