//! Privacy invariant integration test (DEC-027 / SDD-090 / SAD-077). //! //! Every `tracing` event emitted while exercising the public-API //! surface of the PTT subsystem must carry only field names from a //! known allow-list, and never any of the banned key-data field //! names. The test installs a cross-platform recording `Layer`, //! drives the backends and the keymap through realistic scenarios, //! and asserts the captured records honour both invariants. //! //! This test is platform-agnostic to compile — the recording layer //! is generic, the assertions are generic — but the //! Windows-specific dispatcher / backend exercise lives under a //! `#[cfg(target_os = "windows")]` block. On Linux and macOS the //! test exercises only the cross-platform pieces (`AudioTransmitGate`, //! `MissedKeyUpWatchdog`, `PttBinding`, `PttInputClass`) which are //! sufficient to verify the privacy invariant on those hosts. use std::sync::{Arc, Mutex}; use tracing::subscriber::with_default; use tracing_subscriber::layer::{Context, SubscriberExt}; use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::Layer; /// Banned field names per DEC-027 / SDD-090. The test fails fast /// if any emitted record carries one of these names. const BANNED_FIELDS: &[&str] = &[ "vk", "scan_code", "scancode", "keysym", "keysym_string", "key_label", "bound_key", "binding", "platform_key", "VKey", "wVk", "wScan", "kbflags", "mouseflags", "key_code", "virtual_key", "key_sequence", "key_press_history", "key_timing", ]; /// Allow-list of field names that may appear in a PTT-subsystem /// tracing record. Anything outside this set is a privacy /// regression even if it is not on the banned list — the spec /// uses an allow-list precisely to catch new field additions /// before they can leak. const ALLOWED_FIELDS: &[&str] = &[ "backend_id", "bound_input_class", "capability_level", "host_id", "timeout_secs", "error", "event", "message", "target", "level", ]; /// Captured shape of a single tracing event. We hold only field /// names — values would defeat the privacy intent of the test. #[derive(Debug, Clone)] struct Captured { target: String, field_names: Vec, } /// `tracing_subscriber::Layer` that records every emitted event's /// target + field-name set. The records live in a `Mutex` /// shared with the test body. #[derive(Clone, Default)] struct RecordingLayer { records: Arc>>, } impl RecordingLayer { fn snapshot(&self) -> Vec { self.records.lock().unwrap_or_else(|e| e.into_inner()).clone() } } #[derive(Default)] struct NameCollector(Vec); impl tracing::field::Visit for NameCollector { fn record_debug(&mut self, field: &tracing::field::Field, _value: &dyn std::fmt::Debug) { self.0.push(field.name().to_string()); } fn record_str(&mut self, field: &tracing::field::Field, _value: &str) { self.0.push(field.name().to_string()); } fn record_i64(&mut self, field: &tracing::field::Field, _value: i64) { self.0.push(field.name().to_string()); } fn record_u64(&mut self, field: &tracing::field::Field, _value: u64) { self.0.push(field.name().to_string()); } fn record_bool(&mut self, field: &tracing::field::Field, _value: bool) { self.0.push(field.name().to_string()); } } impl Layer for RecordingLayer where S: tracing::Subscriber + for<'a> LookupSpan<'a>, { fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) { let mut names = NameCollector::default(); event.record(&mut names); let captured = Captured { target: event.metadata().target().to_string(), field_names: names.0, }; self.records.lock().unwrap_or_else(|e| e.into_inner()).push(captured); } } /// Assert the captured records honour the DEC-027 banned-field /// rule and the allow-list. Only records from the chanora targets /// participate — host-side tracing-subscriber chatter (e.g. the /// tokio runtime) is ignored. fn assert_privacy_invariants(records: &[Captured]) { let relevant: Vec<&Captured> = records .iter() .filter(|r| r.target.starts_with("chanora_")) .collect(); for r in &relevant { for name in &r.field_names { assert!( !BANNED_FIELDS.contains(&name.as_str()), "banned field {name:?} emitted by target {:?}", r.target ); assert!( ALLOWED_FIELDS.contains(&name.as_str()), "field {name:?} from target {:?} is not in the allow-list", r.target ); } } } #[test] fn ptt_subsystem_never_emits_banned_or_unknown_fields() { let layer = RecordingLayer::default(); let subscriber = tracing_subscriber::registry().with(layer.clone()); with_default(subscriber, || { // Cross-platform paths: AudioTransmitGate transitions // (these emit no tracing themselves but exercise the // public surface); PttBinding / PttInputClass construction. use chanora_audio::ptt_backends::{PttBinding, PttInputClass}; use chanora_audio::AudioTransmitGate; let gate = AudioTransmitGate::new(false); gate.set(true); gate.set(false); let _ = gate.load(); let _ = PttBinding::none(); let _ = PttBinding { input_class: PttInputClass::Keyboard, platform_key: "Space".to_string(), }; let _ = PttInputClass::Keyboard.as_str(); let _ = PttInputClass::MouseSideButton.as_str(); // Platform-specific dispatcher paths. These live behind // the cfg gate; cross-platform we still get coverage of // the cross-platform surface above which is enough to // catch any accidental info!/warn! that names a banned // field at module init time. #[cfg(target_os = "windows")] windows_exercise(); }); let records = layer.snapshot(); assert_privacy_invariants(&records); } #[cfg(target_os = "windows")] fn windows_exercise() { // The Windows backend types are intentionally not part of // chanora_audio's public API (they live in // `crate::ptt_backends::windows` as `pub(crate)`). The privacy // invariant for those code paths is enforced by the dispatcher // unit tests inside `windows.rs` itself, where the same // recording layer pattern is used. From here we simply // exercise the `select_ptt_backend` factory + descriptor + // start/stop lifecycle through the public trait surface so // any info!/warn! the factory or the backend's `start` path // emits is captured by the layer. use chanora_audio::ptt_backends::{PttBinding, PttInputClass}; use chanora_audio::{select_ptt_backend, AudioTransmitGate}; let mut backend = select_ptt_backend(); let gate = AudioTransmitGate::new(false); let binding = PttBinding { input_class: PttInputClass::Keyboard, platform_key: "Space".to_string(), }; // Best-effort: in the CI sandbox the actual Raw Input / // hook registration may fail. The privacy invariant is // about field names, not about whether the start succeeds. let _ = backend.start(gate, binding); let _ = backend.descriptor(); let bad_binding = PttBinding { input_class: PttInputClass::Keyboard, platform_key: "Banana".to_string(), }; let _ = backend.rebind(bad_binding); backend.stop(); }