//! Integration tests for M7P4 Operational Visibility — signal/WAL + index health metrics. //! //! - Task 02: Signal + WAL metrics //! - Task 03: Index health metrics //! //! Every test in this file is gated behind the `metrics` feature. #![allow(clippy::unwrap_used)] #[cfg(feature = "metrics")] use std::time::Duration; #[cfg(feature = "metrics")] use tidaldb::{ TidalDb, schema::{DecaySpec, EntityId, EntityKind, Timestamp, Window}, }; // ── Prometheus output helpers ──────────────────────────────────────────────── /// Extract the numeric value for a metric from Prometheus text output. /// /// Looks for a line that starts with `name` (not a comment), then parses /// the trailing float. Returns `None` if the metric is absent. #[cfg(feature = "metrics")] fn prometheus_value(prom: &str, name: &str) -> Option { prom.lines() .filter(|l| !l.starts_with('#')) .find(|l| l.starts_with(name) && l[name.len()..].starts_with(' ')) .and_then(|l| l.split_whitespace().last()) .and_then(|v| v.parse().ok()) } // ── Shared test schema ─────────────────────────────────────────────────────── #[cfg(feature = "metrics")] fn build_test_schema() -> tidaldb::schema::Schema { use tidaldb::{AgentPolicy, schema::SchemaBuilder}; let mut builder = SchemaBuilder::new(); let _ = builder .signal( "view", EntityKind::Item, DecaySpec::Exponential { half_life: Duration::from_secs(7 * 24 * 3600), }, ) .windows(&[Window::OneHour]) .add(); let _ = builder .signal( "like", EntityKind::Item, DecaySpec::Exponential { half_life: Duration::from_secs(30 * 24 * 3600), }, ) .windows(&[Window::OneHour]) .add(); builder.session_policy( "default", AgentPolicy { allowed_signals: vec!["view".to_string(), "like".to_string()], denied_signals: vec![], max_session_duration: Duration::from_secs(3600), max_signals_per_session: 1000, }, ); builder.build().unwrap() } // ── Task 02: Signal + WAL Metrics ─────────────────────────────────────────── #[cfg(feature = "metrics")] #[test] fn signal_write_increments_counter() { let schema = build_test_schema(); let db = TidalDb::builder() .ephemeral() .with_schema(schema) .open() .unwrap(); let before = prometheus_value( &db.metrics().render_prometheus(), "tidaldb_signal_writes_total", ) .unwrap_or(0.0); db.signal("view", EntityId::new(1), 1.0, Timestamp::now()) .unwrap(); db.signal("like", EntityId::new(2), 1.0, Timestamp::now()) .unwrap(); db.signal("view", EntityId::new(3), 0.5, Timestamp::now()) .unwrap(); let after = prometheus_value( &db.metrics().render_prometheus(), "tidaldb_signal_writes_total", ) .unwrap(); assert!( ((after - before) - 3.0).abs() < 0.5, "signal_writes_total must increment once per signal write" ); } #[cfg(feature = "metrics")] #[test] fn signal_write_latency_appears_in_prometheus() { let schema = build_test_schema(); let db = TidalDb::builder() .ephemeral() .with_schema(schema) .open() .unwrap(); for i in 1u64..=3 { db.signal("view", EntityId::new(i), 1.0, Timestamp::now()) .unwrap(); } let prom = db.metrics().render_prometheus(); assert!( prom.contains("tidaldb_signal_write_latency_us"), "Prometheus output must include signal write latency histogram" ); assert!( prom.contains("tidaldb_signal_writes_total"), "Prometheus output must include signal_writes_total counter" ); assert!( prom.contains("tidaldb_wal_lag_bytes"), "Prometheus output must include wal_lag_bytes gauge" ); assert!( prom.contains("tidaldb_checkpoint_age_seconds"), "Prometheus output must include checkpoint_age_seconds gauge" ); } // ── Task 03: Index Health Metrics ──────────────────────────────────────────── #[cfg(feature = "metrics")] #[test] fn prometheus_output_contains_all_index_gauge_names() { let schema = build_test_schema(); let db = TidalDb::builder() .ephemeral() .with_schema(schema) .open() .unwrap(); let prom = db.metrics().render_prometheus(); assert!( prom.contains("tidaldb_tantivy_segment_count"), "Prometheus output must include tantivy_segment_count gauge" ); assert!( prom.contains("tidaldb_tantivy_indexed_docs"), "Prometheus output must include tantivy_indexed_docs gauge" ); assert!( prom.contains("tidaldb_usearch_vector_count"), "Prometheus output must include usearch_vector_count gauge" ); assert!( prom.contains("tidaldb_usearch_index_size_bytes"), "Prometheus output must include usearch_index_size_bytes gauge" ); assert!( prom.contains("tidaldb_bitmap_index_cardinality"), "Prometheus output must include bitmap_index_cardinality gauge" ); }