tidaldb/tidal/tests/visibility_metrics.rs
jordan cdbe9cb453 Merge remote-tracking branch 'origin/main' (m11/m12 cluster) into m9/m10
Reconciles two independently-developed lines from base 006d3d0:
  ours   — M9/M10 community layers, retroactive purge + re-materialization,
           signal revocation, agent capability boundaries, P1 feedback loop,
           reason labels, instrumented metrics
  theirs — M11/M12 cluster mode (tidal-net gRPC transport, tidal-server
           cluster/scatter-gather, tidal-stress), multi-vector preference,
           ANN candidate-gen, warm-tier day buckets, keyed signal snapshots

Notable semantic resolutions:

* storage::keys::Tag — both sides allocated 0x0E..0x11 for different
  records. Kept theirs' 0x0E..0x1A (shipped on-disk format) and renumbered
  ours to 0x1B..0x1E (CommunityMembership/Revocation/PurgeManifest/
  CommunityLeave); Tag::ALL grown to 30 so the contiguity drift guard holds.

* ranking executor — took theirs' rewrite (SignalReadPlan pre-pass, keyed
  SignalKey snapshots, Result-returning reads, finalize()) and re-applied
  ours' M10 read suppression at the chokepoints it introduced:
  single_signal_score, score_hot/trending/controversial,
  CreatorEngagementRate, and the Stage-4 boost loop.

* signals::warm — theirs' day-bucket/read-time-rotation rewrite, with ours'
  subtract_bucket and Clone extended to the new day tier; ours' test split
  kept (warm/tests.rs, warm/proptests.rs) carrying theirs' updated bodies.

* db::signals — kept ours' contribution-logging try_cohort_attribution in
  signal_dispatch.rs and theirs' event-time try_update_preference_vector;
  dropped the superseded duplicates.

* db::mod / from_parts — theirs' constructors, with ours' purge/
  re-materialization/revocation/community/skip-counter fields and restart
  rebuilds; from_parts kept in its own file per the 600-line guideline.

* schema::validation::builders — ours' module split with theirs' expanded
  tests; policy validation runs both sides' checks (read-signal lists +
  profile overrides, then the zero-duration limit guard).

* feedback Unhide no longer writes a -1.0 "hide" signal: theirs' engine
  rejects negative weights (spec §8). Reverses index state only, matching
  every other undo action.

* SessionState::new is now the single construction path (gains
  overrides_rejected/default_profile); AuditEntry gains kind on the
  deserialize path, inferred from the accepted flag as before.

* Removed tidal/src/replication/tcp_transport.rs and its test: never
  declared in replication/mod.rs on either branch, so it had never
  compiled and nothing referenced it. Superseded by tidal-net's
  GrpcTransport.

Verified: cargo clippy -p tidaldb (lib) clean; --all-targets compiles for
tidaldb/tidal-net/tidal-server/tidal-stress; 2094/2094 lib tests and the
integration suite pass except m8p3_reconcile_production's two CRDT-count
assertions, which fail identically on MERGE_HEAD (pre-existing).
tidalctl cannot build locally: its aws-sdk deps need rustc 1.91.1, local
toolchain is 1.91.0.
2026-08-03 02:16:04 -06:00

177 lines
5.6 KiB
Rust

//! 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::too_many_lines, 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<f64> {
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,
..AgentPolicy::default()
},
);
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"
);
}